Compare commits

...

142 Commits

Author SHA1 Message Date
Sam Potts 840e31a693 v3.3.12 2018-06-11 17:10:37 +10:00
Sam Potts 38f954ef17 Merge branch 'master' into develop
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-11 16:48:54 +10:00
Sam Potts abd1182303 Merge 2018-06-11 16:45:40 +10:00
Sam Potts efe70ab48e v3.3.11 2018-06-11 16:39:35 +10:00
Sam Potts 1ad76800b0 Merge pull request #1024 from friday/event-bubble-detail
Event "detail" is lost in the synthetic event bubble/proxy
2018-06-11 16:13:02 +10:00
Albin Larsson cc97d7be6a Fix synthetic event bubble/proxy loses detail 2018-06-11 08:00:46 +02:00
Sam Potts f951cb372c Merge pull request #1023 from friday/make-utils-static
Make utils static
2018-06-11 14:41:06 +10:00
Albin Larsson 37a3ab202a Remove wrapper function around utils.is.element in Plyr.setup() (no lnger needed) 2018-06-11 05:44:57 +02:00
Albin Larsson b148adc0af Avoid using this to refer to utils or utils.is, since that means methods can't be used statically 2018-06-11 05:44:57 +02:00
Albin Larsson 16828e975a Move utils.is.getConstructor() to utils.getConstructor() 2018-06-11 05:44:57 +02:00
Sam Potts 7d26f41d64 Merge pull request #1015 from friday/captions-fixes-again
Captions rewrite (use index internally to support missing or duplicate languages)
2018-06-11 13:21:05 +10:00
Sam Potts f37f465ce4 Merge pull request #1020 from friday/1016
Vimeo: Update playback state and assure events are triggered on load
2018-06-11 11:48:03 +10:00
Sam Potts b199215525 Merge pull request #1021 from friday/vimeo-seek-while-playing
Fix for YouTube and Vimeo pausing after seek
2018-06-11 11:47:34 +10:00
Albin Larsson 94699f3255 Fix problem with YouTube and Vimeo seeking while playing 2018-06-11 02:21:00 +02:00
Albin Larsson d3e98eb27e Vimeo: Assure state is updated with autoplay (fixes #1016) 2018-06-11 00:00:16 +02:00
Albin Larsson 41012a9843 Typo 2018-06-10 22:00:15 +02:00
Albin Larsson c83487a293 Fix #1017, fix #980, fix #1014: Captions rewrite (use index internally) 2018-06-10 19:00:07 +02:00
Albin Larsson 1fab4919c0 controls.createMenuItem: Change input to object (too many params made it hard to read) 2018-06-10 18:57:19 +02:00
Albin Larsson 9dc0f28800 Avoid condition in getTracks 2018-06-10 18:57:19 +02:00
Albin Larsson b57784d1a5 Change debug warn 'Unsupported language option' to log 'Language option doesn't yet exist' since it doesn't have to be an error 2018-06-10 18:56:13 +02:00
Albin Larsson a80b31bf98 Fix #1003: Formatted captions issue 2018-06-10 18:56:13 +02:00
Sam Potts 76bb299c68 Restore default 2018-06-09 12:05:37 +10:00
Sam Potts 0c03accd41 Fix Sprite issue 2018-06-09 12:04:53 +10:00
Albin Larsson b12eeb0eb7 Merge captions setText and setCue into updateCues (fixes #998 and vimeo cuechange event) 2018-06-08 11:44:15 +02:00
Sam Potts 8e634862ff Merge pull request #1007 from cky917/master
fix:  After clicking on the progress bar, keyboard operations will not work.
2018-06-08 10:54:16 +10:00
Sam Potts 1e1874d86b Merge pull request #1009 from friday/contributing
Contributing document and codepen demo updates
2018-06-08 10:52:37 +10:00
Albin Larsson 16624b90d3 Clarifications due to recent non-constructive comments in #1001 2018-06-07 14:30:05 +02:00
Albin Larsson ed14b656a8 Add contributing document 2018-06-07 11:47:51 +02:00
Albin Larsson 1d0cf16254 Readme: Replace streaming section with codepen templates for all supported formats and libraries (and updated code) 2018-06-07 11:47:34 +02:00
cky 84424f7f67 fix: when the seek input is focused and the video is playing, the space key can't make the video pause, because after 'keyup', it always make the video play 2018-06-06 19:27:07 +08:00
cky c95d9923f7 fix: https://github.com/sampotts/plyr/issues/1006 2018-06-06 16:59:42 +08:00
Sam Potts 05b85da3f4 Update controls.md (fixes #996) 2018-06-02 19:48:48 +10:00
Sam Potts a4caba120c Merge branch 'master' of github.com:sampotts/plyr
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-05-31 23:43:40 +10:00
Sam Potts 969a877a34 v3.3.10 2018-05-31 23:41:48 +10:00
Sam Potts fb22a90d33 Merge pull request #993 from sampotts/develop
v3.3.10
2018-05-31 23:39:51 +10:00
Sam Potts 108bd3dfa0 Fixed incorrect BEM formatting, fixed buffer alignment 2018-05-31 23:33:59 +10:00
Sam Potts 5a445ae647 Merge pull request #988 from kim-company/translate-qualities
Translate quality badges and quality names
2018-05-31 22:43:31 +10:00
Philip Giuliani 56668f58b6 Rename qualityName to label 2018-05-31 14:40:56 +02:00
Sam Potts eec96e5879 Merge pull request #990 from friday/travis
Travis integration
2018-05-31 18:33:00 +10:00
Sam Potts c6c9d877e4 Merge pull request #992 from philipgiuliani/quality-resume-time
Wait for the metadata to be loaded before setting the currentTime
2018-05-31 18:32:10 +10:00
Philip Giuliani 61f4b998e1 Wait for the metadata to be loaded before setting the currentTime 2018-05-31 10:17:41 +02:00
Sam Potts 25a319d884 Merge pull request #989 from friday/pr-template
Proposed changes to PR template
2018-05-31 08:34:02 +10:00
Albin Larsson 4bf678fe6c Proposed changes to PR template (develop as base branch, exclude build and typo fix) 2018-05-30 19:52:15 +02:00
Albin Larsson 359acd6bb9 Lint and build in travis 2018-05-30 19:40:17 +02:00
Albin Larsson 2fce385691 Add npm scripts for linting and building 2018-05-30 19:40:17 +02:00
Albin Larsson a82c61c539 Gulp: Add option to build only 2018-05-30 19:39:57 +02:00
Philip Giuliani a8fa125a96 Refactor getDeep method 2018-05-30 17:30:10 +02:00
Philip Giuliani 6435ced707 Return undefined when the key is not present. 2018-05-30 17:10:38 +02:00
Philip Giuliani 94dc0d176c Rebuild 2018-05-30 17:03:41 +02:00
Philip Giuliani 23c21252e8 Rebuild 2018-05-30 17:02:07 +02:00
Philip Giuliani 41c7dff0e8 Add getDeep method to utils 2018-05-30 17:02:07 +02:00
Philip Giuliani e3bae562fc Rebuild 2018-05-30 17:02:07 +02:00
Philip Giuliani 1c1668bfc3 Implement translation support for qualityName and qualityBadge 2018-05-30 17:02:07 +02:00
Philip Giuliani e0c09c51f2 Allow nested translations 2018-05-30 17:02:07 +02:00
Philip Giuliani 8de06fb862 Accept quality 0 2018-05-30 17:02:07 +02:00
Sam Potts 64412868d8 ESLint tweak 2018-05-30 17:02:07 +02:00
Sam Potts 450958c290 Merge pull request #981 from friday/hls-captions
Improve captions handling for streaming
2018-05-30 21:44:42 +10:00
Sam Potts 963fe11ad6 Update readme.md 2018-05-30 00:22:01 +10:00
Sam Potts ce199e4b6b Merge pull request #986 from friday/701
Call duration update method manually if user config has duration
2018-05-30 00:20:15 +10:00
Albin Larsson 9d798893b5 Call duration update method manually if user config has duration 2018-05-29 16:06:07 +02:00
Albin Larsson 64399e0717 Defer initial captions update to next tick, to avoid event being triggered to early 2018-05-28 17:54:25 +02:00
Albin Larsson f58e23b325 Change to using addtrack and removetrack listeners since 'change' didn't trigger in firefox for embedded captions (may also be a hls.js issue) 2018-05-28 16:19:44 +02:00
Albin Larsson 812e07b734 Replace browser language detection in defaults.js with explicit 'auto' option 2018-05-28 07:43:37 +02:00
Albin Larsson c9298fde76 Fix typo 2018-05-28 07:08:38 +02:00
Albin Larsson 0109454a34 Ensure language is set in case the track is added after initialization, and trigger languagechange event when language is initially set 2018-05-28 06:08:03 +02:00
Albin Larsson 813f703211 Add option to watch caption track changes and update language options 2018-05-28 06:08:03 +02:00
Albin Larsson 7aad747c25 Optimize captions code reused and ensure captionsenabled/captionsdisabled
will be sent on initial setup
2018-05-28 05:44:54 +02:00
Sam Potts d70a787af1 Merge branch 'develop' 2018-05-28 10:42:11 +10:00
Sam Potts 69bb0917ad v3.3.9 2018-05-28 10:41:51 +10:00
Sam Potts 6f256d09b2 ESLint tweak 2018-05-28 10:18:04 +10:00
Sam Potts e9684c2021 Merge pull request #979 from friday/respect-storage
Respect storage being disabled for storage getter
2018-05-28 10:09:02 +10:00
Sam Potts bf91a0e73f Merge pull request #977 from friday/utils.is.icue-ie11
Restore utils.is.cue()
2018-05-28 10:08:42 +10:00
Sam Potts 14b6309aef Merge pull request #978 from friday/ie-issues
Fix InvalidStateError and IE11 issues
2018-05-28 10:08:24 +10:00
Albin Larsson 6391ced99f If storage is disabled, disable get as well, not just set 2018-05-28 01:58:06 +02:00
Albin Larsson fac8a185ba Simplify currentTime setter and bail when media hasn't loaded 2018-05-28 00:57:07 +02:00
Albin Larsson c69aa8a42b Avoid duration getter returning NaN before element has loaded 2018-05-28 00:57:01 +02:00
Albin Larsson f34bf22125 Restore utils.is.cue() 2018-05-27 20:28:48 +02:00
Sam Potts f0be913dc3 Merge pull request #975 from sampotts/develop
v3.3.8
2018-05-26 13:55:22 +10:00
Sam Potts cd51788b98 v3.3.8 2018-05-26 13:53:15 +10:00
Sam Potts edd67b0da3 Typo 2018-05-20 23:44:40 +10:00
Sam Potts d733454d7f Pause while seeking 2018-05-20 23:40:28 +10:00
Sam Potts 41f9a87e0e Add URL polyfill 2018-05-20 23:40:00 +10:00
Sam Potts f4858f0c62 Merge pull request #959 from friday/876
Youtube and vimeo fixes
2018-05-19 16:49:31 +10:00
Albin Larsson 121093ae71 Prevent durationchange events from showing time when invertTime is false 2018-05-19 04:27:45 +02:00
Albin Larsson aa8fc313a9 Fix #966: Add 'seeked' event listener to update progress (seeking doesn't have the correct time) 2018-05-19 04:23:22 +02:00
Albin Larsson 723298a07b Fix #921: Trigger seeked event in youtube plugin if either playing or paused 2018-05-19 04:18:27 +02:00
Albin Larsson f8c89e3e95 Fix #876: YouTube and Vimeo autoplays on seek 2018-05-19 04:18:27 +02:00
Albin Larsson 333435a9c2 Fix playback state (paused) and events (play/pause) 2018-05-19 04:18:27 +02:00
Sam Potts 3ab2295fe7 Merge branch 'master' into develop 2018-05-19 11:29:47 +10:00
Sam Potts c41bb657ac Merge pull request #958 from friday/954
Fix the seek tooltip time difference from seek time
2018-05-19 11:28:38 +10:00
Sam Potts 55bbf64f2b Merge pull request #963 from friday/verify-poster
Make sure poster element isn't shown if the image isn't loaded
2018-05-19 11:27:52 +10:00
Sam Potts 3bba65f2c2 Merge pull request #967 from friday/883
toggleControls rewrite
2018-05-19 11:27:19 +10:00
Sam Potts 1bab0d07b5 Merge branch 'master' into develop 2018-05-19 11:26:05 +10:00
Sam Potts 602353f4d9 Merge branch 'master' of github.com:sampotts/plyr 2018-05-19 11:25:02 +10:00
Sam Potts 51814249af Reduce circular dependencies 2018-05-19 11:24:56 +10:00
Albin Larsson 37c5fbfe16 toggleControls() rewrite 2018-05-18 16:19:38 +02:00
Albin Larsson d7356726a1 Remove ui.checkFailed() and error class 2018-05-16 23:21:06 +02:00
Albin Larsson 4db6bf7a2e Make utils.toggleClass() compatible with Element.classList.toggle (rename toggle argument to 'force' and make it optional) 2018-05-16 23:21:06 +02:00
Albin Larsson 28826f6402 Add 'video only' caveat to toggleControls() doc (current behavior) 2018-05-16 23:21:06 +02:00
Albin Larsson c845558d96 Youtube poster: Set css backgroundSize to 'cover' for padded youtube thumbnails 2018-05-15 16:41:51 +02:00
Albin Larsson 16c3a7d9e5 Rewrite ui.setPoster to check that images arent broken or youtube fallback images. Only show poster element when valid 2018-05-15 16:21:36 +02:00
Albin Larsson 90d5b48845 Add async method to utils for loading/checking images 2018-05-15 13:27:55 +02:00
Albin Larsson d1acc4abb3 Add event before seeking via mouse interaction to set alternative 'value' for the input matching the tooltip time 2018-05-14 19:50:08 +02:00
Sam Potts 797b70998f Merge pull request #960 from friday/935
Support importing Plyr in Node.js without errors
2018-05-14 23:38:48 +10:00
Sam Potts 4a01027da0 Merge pull request #961 from friday/expose-defaults
Enable overriding defaults
2018-05-14 23:38:25 +10:00
Albin Larsson 7ca2169790 Expose defaults (enable overriding) 2018-05-14 06:49:04 +02:00
Albin Larsson 054f522aa9 Enable importing Plyr in node.js without errors (resulting in an empty object) 2018-05-14 05:35:13 +02:00
Albin Larsson f2fc3f5ea5 Fix the seek tooltip time difference from seek time 2018-05-12 00:10:39 +02:00
Sam Potts 765c01e83d Remove references to window.Plyr 2018-05-10 09:34:15 +10:00
Sam Potts 33a11fb53a v3.3.7 2018-05-09 09:50:22 +10:00
Sam Potts d1d41ca49a Merge branch 'master' of github.com:sampotts/plyr 2018-05-09 09:48:52 +10:00
Sam Potts c06e0ee5e9 Grid tweak 2018-05-09 09:48:46 +10:00
Sam Potts 83f80ccc40 Merge pull request #950 from friday/poster-fixes
Poster fixes
2018-05-09 09:44:36 +10:00
Albin Larsson 069065ea3a Fix #946 - poster getting click events 2018-05-08 16:50:40 +02:00
Albin Larsson 1672e78041 Fix poster being stretched 2018-05-08 16:49:32 +02:00
Sam Potts 34401de3d0 Merge branch 'master' into develop 2018-05-08 22:22:43 +10:00
Sam Potts f687b81b70 v3.3.6 2018-05-08 13:18:30 +10:00
Sam Potts bbb11e611e Vimeo options, docs for multiple players 2018-05-08 13:12:39 +10:00
Sam Potts 90919411e9 Use div for poster, Vimeo fixes, Tooltip fixes 2018-05-08 12:57:24 +10:00
Sam Potts 1491b017a0 Setup multiple players 2018-05-06 16:18:10 +10:00
Sam Potts 1655150092 v3.3.5 2018-05-06 01:32:51 +10:00
Sam Potts ceb6c9a100 v3.3.3 2018-05-06 01:16:41 +10:00
Sam Potts 00bbce08fb Reverted menu change 2018-05-06 01:14:41 +10:00
Sam Potts 91a4b86860 Small bug fixes 2018-05-06 01:03:38 +10:00
Sam Potts 5aece6fa06 Merge 2018-05-06 00:50:02 +10:00
Sam Potts a70b94afe2 Merge branch 'master' of github.com:sampotts/plyr 2018-05-06 00:49:22 +10:00
Sam Potts 9ebc2719d3 v3.3.0 2018-05-06 00:49:12 +10:00
Sam Potts b46aae1833 Merge pull request #939 from Billybobbonnet/patch-1
Added 480p to SD labels
2018-05-03 20:28:03 +10:00
Antoine Cordelois 30e6a40865 Added 480p to SD labels 2018-05-03 11:42:09 +02:00
Sam Potts 403df36af6 Merge branch 'master' into develop 2018-04-27 21:42:29 +10:00
Sam Potts 5ca769807e Merge pull request #923 from friday/922
Only add hideControls class if config.hideControls is truthy
2018-04-27 20:07:18 +10:00
Sam Potts 72a71a605b Fix for default timestamp 2018-04-27 20:06:14 +10:00
Sam Potts 24d833a5d1 Merge branch 'master' into develop 2018-04-27 18:35:06 +10:00
Sam Potts 44b30380f7 Merge branch 'beta' of github.com:Selz/plyr 2018-04-27 18:34:06 +10:00
Sam Potts 261cd086c7 Update readme.md 2018-04-27 12:44:58 +10:00
Albin Larsson 9e19b526b9 Only add hideControls class if config.hideControls is truthy 2018-04-26 17:51:14 +02:00
Sam Potts 6c617a0ef1 Readme fix 2018-04-27 01:12:04 +10:00
Sam Potts a812650fea v3.2.4 2018-04-27 00:47:51 +10:00
Sam Potts fec7a77d6f v3.2.3 2018-04-25 20:02:36 +10:00
Sam Potts 971e261067 Fix for iOS 9 throwing error for name property in fullscreen API (fixes #908) 2018-04-25 19:59:22 +10:00
Sam Potts f13260c10a Merge pull request #919 from sampotts/master
Merge back
2018-04-25 07:38:17 +10:00
Sam Potts e1183d6049 Merge pull request #918 from sampotts/master
Merge back
2018-04-25 07:37:18 +10:00
55 changed files with 8921 additions and 7228 deletions
+1
View File
@@ -32,6 +32,7 @@
"message": "Use local parameter instead." "message": "Use local parameter instead."
} }
], ],
"no-param-reassign": [2, { "props": false }],
"array-bracket-newline": [2, { "minItems": 2 }], "array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }] "array-element-newline": [2, { "minItems": 2 }]
}, },
+5 -5
View File
@@ -1,8 +1,8 @@
### Link to related issue (if applicable) ### Link to related issue (if applicable)
### Sumary of proposed changes ### Summary of proposed changes
### Task list ### Checklist
- [ ] Use `develop` as the base branch
- [ ] Tested on [supported browsers](https://github.com/sampotts/plyr#browser-support) - [ ] Exclude the gulp build from the PR
- [ ] Gulp build completed - [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
+7
View File
@@ -0,0 +1,7 @@
language: node_js
node_js:
- 'lts/*'
script:
- npm run lint
- npm run build
+52
View File
@@ -0,0 +1,52 @@
# Contributing
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
## Reporting issues
Our GitHub issue tracker is for bug reports and feature requests. Don't create support issues here. Use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that.
Please verify that your issue hasn't already been answered by our FAQ (https://github.com/sampotts/plyr/wiki/FAQ), or that there isn't already an open issue for it.
When applicable, check that your problem doesn't happen without Plyr (see [FAQ#1](https://github.com/sampotts/plyr/wiki/FAQ#1-does-plyr-work-with--)).
Verify that you are following the documentation, are using the latest version of Plyr, and aren't getting any errors in your own code, causing the issues.
Describe the issue as detailed as possible, answering these questions:
* Does it happen only with specific options and/or specific browsers?
* Does is happen only with HTML5 video, audio, youtube, vimeo or a specific library?
* Does the issue happen on [our demo](https://plyr.io/)? If not, please recreate it with a **minimal** example online. You can use our Codepen templates to get started:
* [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)
* [Dash.js integration](https://codepen.io/pen?template=zaBgBy)
* [Hls.js integration](https://codepen.io/pen?template=oyLKQb)
* [Shaka Player integration](https://codepen.io/pen?template=ZRpzZO)
It's important that you keep the issue description and replication demo **minimal**. If your implementation is using a framework, library or custom methods, which aren't needed to reproduce the issue, this makes it harder to debug and understand the issue. While it may be relevant to bring this up (ex: "I need Plyr to trigger the event sooner or it breaks Framework X") it also means that the person who is trying to fix the issue either has to know or learn your frameworks, libraries and custom methods, or that no one will try to fix your issue because it's too much work.
In order to keep things on topic and to avoid bothering people with github notifications, please don't combine multiple problems or bugs into one issue, don't comment on issues unless your comment is related to that issue, and don't post "+1" or "I agree" type of comments. Use the emojis instead.
Last but not least: Keep a civil tone in issues and comments. Non-constructive comments may be removed.
## Requesting features and improvements
If you are missing something in Plyr, you can create a GitHub issue for this as well. Since we prioritize fixing bugs first, and may have a lot of other suggestions and architectural changes to work on as well, these may not be at the top of our list. If it's important or urgent to you, you may want to first ensure it's something we want to have in Plyr, and then contribute it as a pull request.
## Contributing features and documentation
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
* Develop and test your modifications.
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as logging or breakpoints you added for testing, and the build output.
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
+453 -358
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -116,12 +116,12 @@ const controls = `
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span> <span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
</button> </button>
<div class="plyr__progress"> <div class="plyr__progress">
<label for="plyr-seek-{id}" class="plyr__sr-only">Seek</label> <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" id="plyr-seek-{id}"> <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span> <span role="tooltip" class="plyr__tooltip">00:00</span>
</div> </div>
<div class="plyr__time">00:00</div> <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute"> <button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg> <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
@@ -129,8 +129,7 @@ const controls = `
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span> <span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
</button> </button>
<div class="plyr__volume"> <div class="plyr__volume">
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label> <input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
</div> </div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions"> <button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
+1 -1
View File
File diff suppressed because one or more lines are too long
+290 -41
View File
@@ -1,4 +1,4 @@
(function () { typeof navigator === "object" && (function () {
'use strict'; 'use strict';
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -97,7 +97,7 @@ function isObject(what) {
// Yanked from https://git.io/vS8DV re-used under CC0 // Yanked from https://git.io/vS8DV re-used under CC0
// with some tiny modifications // with some tiny modifications
function isError(value) { function isError(value) {
switch ({}.toString.call(value)) { switch (Object.prototype.toString.call(value)) {
case '[object Error]': case '[object Error]':
return true; return true;
case '[object Exception]': case '[object Exception]':
@@ -110,7 +110,15 @@ function isError(value) {
} }
function isErrorEvent(value) { function isErrorEvent(value) {
return supportsErrorEvent() && {}.toString.call(value) === '[object ErrorEvent]'; return Object.prototype.toString.call(value) === '[object ErrorEvent]';
}
function isDOMError(value) {
return Object.prototype.toString.call(value) === '[object DOMError]';
}
function isDOMException(value) {
return Object.prototype.toString.call(value) === '[object DOMException]';
} }
function isUndefined(what) { function isUndefined(what) {
@@ -153,6 +161,24 @@ function supportsErrorEvent() {
} }
} }
function supportsDOMError() {
try {
new DOMError(''); // eslint-disable-line no-new
return true;
} catch (e) {
return false;
}
}
function supportsDOMException() {
try {
new DOMException(''); // eslint-disable-line no-new
return true;
} catch (e) {
return false;
}
}
function supportsFetch() { function supportsFetch() {
if (!('fetch' in _window)) return false; if (!('fetch' in _window)) return false;
@@ -245,7 +271,13 @@ function objectFrozen(obj) {
} }
function truncate(str, max) { function truncate(str, max) {
return !max || str.length <= max ? str : str.substr(0, max) + '\u2026'; if (typeof max !== 'number') {
throw new Error('2nd argument to `truncate` function should be a number');
}
if (typeof str !== 'string' || max === 0) {
return str;
}
return str.length <= max ? str : str.substr(0, max) + '\u2026';
} }
/** /**
@@ -544,10 +576,9 @@ function jsonSize(value) {
} }
function serializeValue(value) { function serializeValue(value) {
var maxLength = 40;
if (typeof value === 'string') { if (typeof value === 'string') {
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026'; var maxLength = 40;
return truncate(value, maxLength);
} else if ( } else if (
typeof value === 'number' || typeof value === 'number' ||
typeof value === 'boolean' || typeof value === 'boolean' ||
@@ -663,6 +694,8 @@ var utils = {
isObject: isObject, isObject: isObject,
isError: isError, isError: isError,
isErrorEvent: isErrorEvent, isErrorEvent: isErrorEvent,
isDOMError: isDOMError,
isDOMException: isDOMException,
isUndefined: isUndefined, isUndefined: isUndefined,
isFunction: isFunction, isFunction: isFunction,
isPlainObject: isPlainObject, isPlainObject: isPlainObject,
@@ -670,6 +703,8 @@ var utils = {
isArray: isArray, isArray: isArray,
isEmptyObject: isEmptyObject, isEmptyObject: isEmptyObject,
supportsErrorEvent: supportsErrorEvent, supportsErrorEvent: supportsErrorEvent,
supportsDOMError: supportsDOMError,
supportsDOMException: supportsDOMException,
supportsFetch: supportsFetch, supportsFetch: supportsFetch,
supportsReferrerPolicy: supportsReferrerPolicy, supportsReferrerPolicy: supportsReferrerPolicy,
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent, supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
@@ -724,10 +759,25 @@ var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Ran
function getLocationHref() { function getLocationHref() {
if (typeof document === 'undefined' || document.location == null) return ''; if (typeof document === 'undefined' || document.location == null) return '';
return document.location.href; return document.location.href;
} }
function getLocationOrigin() {
if (typeof document === 'undefined' || document.location == null) return '';
// Oh dear IE10...
if (!document.location.origin) {
return (
document.location.protocol +
'//' +
document.location.hostname +
(document.location.port ? ':' + document.location.port : '')
);
}
return document.location.origin;
}
/** /**
* TraceKit.report: cross-browser processing of unhandled exceptions * TraceKit.report: cross-browser processing of unhandled exceptions
* *
@@ -1135,6 +1185,44 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
element.func = UNKNOWN_FUNCTION; element.func = UNKNOWN_FUNCTION;
} }
if (element.url && element.url.substr(0, 5) === 'blob:') {
// Special case for handling JavaScript loaded into a blob.
// We use a synchronous AJAX request here as a blob is already in
// memory - it's not making a network request. This will generate a warning
// in the browser console, but there has already been an error so that's not
// that much of an issue.
var xhr = new XMLHttpRequest();
xhr.open('GET', element.url, false);
xhr.send(null);
// If we failed to download the source, skip this patch
if (xhr.status === 200) {
var source = xhr.responseText || '';
// We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file.
// Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175
source = source.slice(-300);
// Now we dig out the source map URL
var sourceMaps = source.match(/\/\/# sourceMappingURL=(.*)$/);
// If we don't find a source map comment or we find more than one, continue on to the next element.
if (sourceMaps) {
var sourceMapAddress = sourceMaps[1];
// Now we check to see if it's a relative URL.
// If it is, convert it to an absolute one.
if (sourceMapAddress.charAt(0) === '~') {
sourceMapAddress = getLocationOrigin() + sourceMapAddress.slice(1);
}
// Now we strip the '.map' off of the end of the URL and update the
// element so that Sentry can match the map to the blob.
element.url = sourceMapAddress.slice(0, -4);
}
}
}
stack.push(element); stack.push(element);
} }
@@ -1646,10 +1734,12 @@ var console$1 = {
var isErrorEvent$1 = utils.isErrorEvent;
var isDOMError$1 = utils.isDOMError;
var isDOMException$1 = utils.isDOMException;
var isError$1 = utils.isError; var isError$1 = utils.isError;
var isObject$1 = utils.isObject; var isObject$1 = utils.isObject;
var isPlainObject$1 = utils.isPlainObject; var isPlainObject$1 = utils.isPlainObject;
var isErrorEvent$1 = utils.isErrorEvent;
var isUndefined$1 = utils.isUndefined; var isUndefined$1 = utils.isUndefined;
var isFunction$1 = utils.isFunction; var isFunction$1 = utils.isFunction;
var isString$1 = utils.isString; var isString$1 = utils.isString;
@@ -1777,7 +1867,7 @@ Raven.prototype = {
// webpack (using a build step causes webpack #1617). Grunt verifies that // webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build. // this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465 // See: https://github.com/getsentry/raven-js/issues/465
VERSION: '3.24.0', VERSION: '3.26.1',
debug: false, debug: false,
@@ -1943,7 +2033,7 @@ Raven.prototype = {
if (isFunction$1(options)) { if (isFunction$1(options)) {
args = func || []; args = func || [];
func = options; func = options;
options = undefined; options = {};
} }
return this.wrap(options, func).apply(this, args); return this.wrap(options, func).apply(this, args);
@@ -1954,7 +2044,7 @@ Raven.prototype = {
* *
* @param {object} options A specific set of options for this context [optional] * @param {object} options A specific set of options for this context [optional]
* @param {function} func The function to be wrapped in a new context * @param {function} func The function to be wrapped in a new context
* @param {function} func A function to call before the try/catch wrapper [optional, private] * @param {function} _before A function to call before the try/catch wrapper [optional, private]
* @return {function} The newly wrapped functions with a context * @return {function} The newly wrapped functions with a context
*/ */
wrap: function(options, func, _before) { wrap: function(options, func, _before) {
@@ -2066,7 +2156,12 @@ Raven.prototype = {
*/ */
_promiseRejectionHandler: function(event) { _promiseRejectionHandler: function(event) {
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event); this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
this.captureException(event.reason); this.captureException(event.reason, {
mechanism: {
type: 'onunhandledrejection',
handled: false
}
});
}, },
/** /**
@@ -2105,6 +2200,23 @@ Raven.prototype = {
if (isErrorEvent$1(ex) && ex.error) { if (isErrorEvent$1(ex) && ex.error) {
// If it is an ErrorEvent with `error` property, extract it to get actual Error // If it is an ErrorEvent with `error` property, extract it to get actual Error
ex = ex.error; ex = ex.error;
} else if (isDOMError$1(ex) || isDOMException$1(ex)) {
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
// then we just extract the name and message, as they don't provide anything else
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
var name = ex.name || (isDOMError$1(ex) ? 'DOMError' : 'DOMException');
var message = ex.message ? name + ': ' + ex.message : name;
return this.captureMessage(
message,
objectMerge$1(options, {
// neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well
// but it's barely any overhead so we may at least try
stacktrace: true,
trimHeadFrames: options.trimHeadFrames + 1
})
);
} else if (isError$1(ex)) { } else if (isError$1(ex)) {
// we have a real Error object // we have a real Error object
ex = ex; ex = ex;
@@ -2116,6 +2228,7 @@ Raven.prototype = {
ex = new Error(options.message); ex = new Error(options.message);
} else { } else {
// If none of previous checks were valid, then it means that // If none of previous checks were valid, then it means that
// it's not a DOMError/DOMException
// it's not a plain Object // it's not a plain Object
// it's not a valid ErrorEvent (one with an error property) // it's not a valid ErrorEvent (one with an error property)
// it's not an Error // it's not an Error
@@ -2207,6 +2320,14 @@ Raven.prototype = {
// stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1] // stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
var initialCall = isArray$1(stack.stack) && stack.stack[1]; var initialCall = isArray$1(stack.stack) && stack.stack[1];
// if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call
// to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd
// initialCall => captureException(string) => captureMessage(string)
if (initialCall && initialCall.func === 'Raven.captureException') {
initialCall = stack.stack[2];
}
var fileurl = (initialCall && initialCall.url) || ''; var fileurl = (initialCall && initialCall.url) || '';
if ( if (
@@ -2715,7 +2836,15 @@ Raven.prototype = {
} }
var originalCallback = args[0]; var originalCallback = args[0];
if (isFunction$1(originalCallback)) { if (isFunction$1(originalCallback)) {
args[0] = self.wrap(originalCallback); args[0] = self.wrap(
{
mechanism: {
type: 'instrument',
data: {function: orig.name}
}
},
originalCallback
);
} }
// IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
@@ -2742,7 +2871,15 @@ Raven.prototype = {
// preserve arity // preserve arity
try { try {
if (fn && fn.handleEvent) { if (fn && fn.handleEvent) {
fn.handleEvent = self.wrap(fn.handleEvent); fn.handleEvent = self.wrap(
{
mechanism: {
type: 'instrument',
data: {target: global, function: 'handleEvent', handler: fn.name}
}
},
fn.handleEvent
);
} }
} catch (err) { } catch (err) {
// can sometimes get 'Permission denied to access property "handle Event' // can sometimes get 'Permission denied to access property "handle Event'
@@ -2782,7 +2919,20 @@ Raven.prototype = {
return orig.call( return orig.call(
this, this,
evtName, evtName,
self.wrap(fn, undefined, before), self.wrap(
{
mechanism: {
type: 'instrument',
data: {
target: global,
function: 'addEventListener',
handler: fn.name
}
}
},
fn,
before
),
capture, capture,
secure secure
); );
@@ -2816,7 +2966,17 @@ Raven.prototype = {
'requestAnimationFrame', 'requestAnimationFrame',
function(orig) { function(orig) {
return function(cb) { return function(cb) {
return orig(self.wrap(cb)); return orig(
self.wrap(
{
mechanism: {
type: 'instrument',
data: {function: 'requestAnimationFrame', handler: orig.name}
}
},
cb
)
);
}; };
}, },
wrappedBuiltIns wrappedBuiltIns
@@ -2879,7 +3039,15 @@ Raven.prototype = {
function wrapProp(prop, xhr) { function wrapProp(prop, xhr) {
if (prop in xhr && isFunction$1(xhr[prop])) { if (prop in xhr && isFunction$1(xhr[prop])) {
fill$1(xhr, prop, function(orig) { fill$1(xhr, prop, function(orig) {
return self.wrap(orig); return self.wrap(
{
mechanism: {
type: 'instrument',
data: {function: prop, handler: orig.name}
}
},
orig
);
}); // intentionally don't track filled methods on XHR instances }); // intentionally don't track filled methods on XHR instances
} }
} }
@@ -2944,7 +3112,19 @@ Raven.prototype = {
xhr, xhr,
'onreadystatechange', 'onreadystatechange',
function(orig) { function(orig) {
return self.wrap(orig, undefined, onreadystatechangeHandler); return self.wrap(
{
mechanism: {
type: 'instrument',
data: {
function: 'onreadystatechange',
handler: orig.name
}
}
},
orig,
onreadystatechangeHandler
);
} /* intentionally don't track this instrumentation */ } /* intentionally don't track this instrumentation */
); );
} else { } else {
@@ -3004,17 +3184,30 @@ Raven.prototype = {
status_code: null status_code: null
}; };
return origFetch.apply(this, args).then(function(response) { return origFetch
fetchData.status_code = response.status; .apply(this, args)
.then(function(response) {
fetchData.status_code = response.status;
self.captureBreadcrumb({ self.captureBreadcrumb({
type: 'http', type: 'http',
category: 'fetch', category: 'fetch',
data: fetchData data: fetchData
});
return response;
})
['catch'](function(err) {
// if there is an error performing the request
self.captureBreadcrumb({
type: 'http',
category: 'fetch',
data: fetchData,
level: 'error'
});
throw err;
}); });
return response;
});
}; };
}, },
wrappedBuiltIns wrappedBuiltIns
@@ -3027,7 +3220,7 @@ Raven.prototype = {
if (_document.addEventListener) { if (_document.addEventListener) {
_document.addEventListener('click', self._breadcrumbEventHandler('click'), false); _document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
_document.addEventListener('keypress', self._keypressEventHandler(), false); _document.addEventListener('keypress', self._keypressEventHandler(), false);
} else { } else if (_document.attachEvent) {
// IE8 Compatibility // IE8 Compatibility
_document.attachEvent('onclick', self._breadcrumbEventHandler('click')); _document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
_document.attachEvent('onkeypress', self._keypressEventHandler()); _document.attachEvent('onkeypress', self._keypressEventHandler());
@@ -3043,8 +3236,8 @@ Raven.prototype = {
var hasPushAndReplaceState = var hasPushAndReplaceState =
!isChromePackagedApp && !isChromePackagedApp &&
_window$2.history && _window$2.history &&
history.pushState && _window$2.history.pushState &&
history.replaceState; _window$2.history.replaceState;
if (autoBreadcrumbs.location && hasPushAndReplaceState) { if (autoBreadcrumbs.location && hasPushAndReplaceState) {
// TODO: remove onpopstate handler on uninstall() // TODO: remove onpopstate handler on uninstall()
var oldOnPopState = _window$2.onpopstate; var oldOnPopState = _window$2.onpopstate;
@@ -3073,8 +3266,8 @@ Raven.prototype = {
}; };
}; };
fill$1(history, 'pushState', historyReplacementFunction, wrappedBuiltIns); fill$1(_window$2.history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
fill$1(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns); fill$1(_window$2.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
} }
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) { if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
@@ -3155,10 +3348,16 @@ Raven.prototype = {
return globalServer; return globalServer;
}, },
_handleOnErrorStackInfo: function() { _handleOnErrorStackInfo: function(stackInfo, options) {
options = options || {};
options.mechanism = options.mechanism || {
type: 'onerror',
handled: false
};
// if we are intentionally ignoring errors via onerror, bail out // if we are intentionally ignoring errors via onerror, bail out
if (!this._ignoreOnError) { if (!this._ignoreOnError) {
this._handleStackInfo.apply(this, arguments); this._handleStackInfo(stackInfo, options);
} }
}, },
@@ -3290,11 +3489,24 @@ Raven.prototype = {
} }
] ]
}, },
culprit: fileurl transaction: fileurl
}, },
options options
); );
// Move mechanism from options to exception interface
// We do this, as requiring user to pass `{exception:{mechanism:{ ... }}}` would be
// too much
if (!data.exception.mechanism && data.mechanism) {
data.exception.mechanism = data.mechanism;
delete data.mechanism;
}
data.exception.mechanism = objectMerge$1(data.exception.mechanism || {}, {
type: 'generic',
handled: true
});
// Fire away! // Fire away!
this._send(data); this._send(data);
}, },
@@ -3364,7 +3576,7 @@ Raven.prototype = {
if (this._hasNavigator && _navigator.userAgent) { if (this._hasNavigator && _navigator.userAgent) {
httpData.headers = { httpData.headers = {
'User-Agent': navigator.userAgent 'User-Agent': _navigator.userAgent
}; };
} }
@@ -3405,7 +3617,7 @@ Raven.prototype = {
if ( if (
!last || !last ||
current.message !== last.message || // defined for captureMessage current.message !== last.message || // defined for captureMessage
current.culprit !== last.culprit // defined for captureException/onerror current.transaction !== last.transaction // defined for captureException/onerror
) )
return false; return false;
@@ -3750,7 +3962,11 @@ Raven.prototype = {
}, },
_logDebug: function(level) { _logDebug: function(level) {
if (this._originalConsoleMethods[level] && this.debug) { // We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change
if (
this._originalConsoleMethods[level] &&
(this.debug || this._globalOptions.debug)
) {
// In IE<10 console methods do not have their own 'apply' method // In IE<10 console methods do not have their own 'apply' method
Function.prototype.apply.call( Function.prototype.apply.call(
this._originalConsoleMethods[level], this._originalConsoleMethods[level],
@@ -3823,11 +4039,11 @@ var singleton = Raven$1;
* const someAppReporter = new Raven.Client(); * const someAppReporter = new Raven.Client();
* const someOtherAppReporter = new Raven.Client(); * const someOtherAppReporter = new Raven.Client();
* *
* someAppReporter('__DSN__', { * someAppReporter.config('__DSN__', {
* ...config goes here * ...config goes here
* }); * });
* *
* someOtherAppReporter('__OTHER_DSN__', { * someOtherAppReporter.config('__OTHER_DSN__', {
* ...config goes here * ...config goes here
* }); * });
* *
@@ -3914,6 +4130,39 @@ singleton.Client = Client;
'airplay', 'airplay',
'fullscreen', 'fullscreen',
], */ ], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: { captions: {
active: true active: true
}, },
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -171,7 +171,7 @@
</aside> </aside>
<!-- Polyfills --> <!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values" <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<!-- Plyr core script --> <!-- Plyr core script -->
+33
View File
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
'airplay', 'airplay',
'fullscreen', 'fullscreen',
], */ ], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: { captions: {
active: true, active: true,
}, },
+1
View File
@@ -29,6 +29,7 @@ video {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 3;
} }
// Style full supported player // Style full supported player
+1 -1
View File
@@ -2,4 +2,4 @@
// Layout // Layout
// ========================================================================== // ==========================================================================
$container-max-width: 1280px; $container-max-width: 1260px;
+1 -1
View File
File diff suppressed because one or more lines are too long
+3054 -2763
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3402 -2762
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+18 -10
View File
@@ -13,6 +13,7 @@ const filter = require('gulp-filter');
const sass = require('gulp-sass'); const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css'); const cleancss = require('gulp-clean-css');
const run = require('run-sequence'); const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer'); const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch'); const gitbranch = require('git-branch');
const svgstore = require('gulp-svgstore'); const svgstore = require('gulp-svgstore');
@@ -129,7 +130,7 @@ const build = {
tasks.js.push(name); tasks.js.push(name);
const { output } = paths[bundle]; const { output } = paths[bundle];
gulp.task(name, () => return gulp.task(name, () =>
gulp gulp
.src(bundles[bundle].js[key]) .src(bundles[bundle].js[key])
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
@@ -146,6 +147,7 @@ const build = {
options, options,
), ),
) )
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(sourcemaps.write('')) .pipe(sourcemaps.write(''))
.pipe(gulp.dest(output)) .pipe(gulp.dest(output))
.pipe(filter('**/*.js')) .pipe(filter('**/*.js'))
@@ -162,7 +164,7 @@ const build = {
const name = `sass:${key}`; const name = `sass:${key}`;
tasks.sass.push(name); tasks.sass.push(name);
gulp.task(name, () => return gulp.task(name, () =>
gulp gulp
.src(bundles[bundle].sass[key]) .src(bundles[bundle].sass[key])
.pipe(sass()) .pipe(sass())
@@ -180,7 +182,7 @@ const build = {
tasks.sprite.push(name); tasks.sprite.push(name);
// Process Icons // Process Icons
gulp.task(name, () => return gulp.task(name, () =>
gulp gulp
.src(paths[bundle].src.sprite) .src(paths[bundle].src.sprite)
.pipe( .pipe(
@@ -224,9 +226,14 @@ gulp.task('watch', () => {
gulp.watch(paths.demo.src.sass, tasks.sass); gulp.watch(paths.demo.src.sass, tasks.sass);
}); });
// Build distribution
gulp.task('build', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite);
});
// Default gulp task // Default gulp task
gulp.task('default', () => { gulp.task('default', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'watch'); run('build', 'watch');
}); });
// Publish a version to CDN and demo // Publish a version to CDN and demo
@@ -239,11 +246,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
const branch = { const branch = {
current: gitbranch.sync(), current: gitbranch.sync(),
master: 'master', master: 'master',
beta: 'beta', develop: 'develop',
}; };
const allowed = [ const allowed = [
branch.master, branch.master,
branch.beta, branch.develop,
]; ];
const maxAge = 31536000; // 1 year const maxAge = 31536000; // 1 year
@@ -255,7 +262,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
}, },
}, },
demo: { demo: {
uploadPath: branch.current === branch.beta ? 'beta/' : null, uploadPath: branch.current === branch.develop ? 'beta/' : null,
headers: { headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding', Vary: 'Accept-Encoding',
@@ -287,7 +294,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
'plyr.polyfilled.js', 'plyr.polyfilled.js',
'defaults.js', 'defaults.js',
]; ];
gulp
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`))) .src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`)) .pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`)) .pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
@@ -406,7 +414,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
}); });
// Do everything // Do everything
gulp.task('publish', () => { gulp.task('publish', callback => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo'); run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
}); });
} }
+18 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.2.2", "version": "3.3.12",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"main": "./dist/plyr.js", "main": "./dist/plyr.js",
@@ -8,45 +8,48 @@
"sass": "./src/sass/plyr.scss", "sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css", "style": "./dist/plyr.css",
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.3",
"babel-eslint": "^8.2.3", "babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0", "babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.7.0",
"del": "^3.0.0", "del": "^3.0.0",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.11.0", "eslint-plugin-import": "^2.12.0",
"git-branch": "^2.0.1", "git-branch": "^2.0.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0", "gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0", "gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.3", "gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0", "gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1", "gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2", "gulp-postcss": "^7.0.1",
"gulp-replace": "^0.6.1", "gulp-rename": "^1.2.3",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0", "gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1", "gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0", "gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4", "gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4", "gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1", "gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.1", "gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"postcss-custom-properties": "^7.0.0",
"prettier-eslint": "^8.8.1", "prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.4", "rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.0", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",
"stylelint": "^9.2.0", "stylelint": "^9.2.1",
"stylelint-config-prettier": "^3.2.0", "stylelint-config-prettier": "^3.2.0",
"stylelint-config-recommended": "^2.1.0", "stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0", "stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1", "stylelint-order": "^0.8.1",
"stylelint-scss": "^3.0.1", "stylelint-scss": "^3.1.0",
"stylelint-selector-bem-pattern": "^2.0.0" "stylelint-selector-bem-pattern": "^2.0.0"
}, },
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"], "keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@@ -62,6 +65,8 @@
"doc": "readme.md" "doc": "readme.md"
}, },
"scripts": { "scripts": {
"build": "gulp build",
"lint": "eslint src/js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
@@ -69,6 +74,7 @@
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0", "custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4", "loadjs": "^3.5.4",
"raven-js": "^3.24.0" "raven-js": "^3.26.1",
"url-polyfill": "^1.0.13"
} }
} }
+66 -46
View File
@@ -39,18 +39,22 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks: Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link | | Type | Maintainer | Link |
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) | | WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) | | React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) | | Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) | | Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) | | Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
## Quick setup ## Quick setup
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`. Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`.
### Try Plyr online
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=zaBgBy), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
### HTML ### HTML
Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types. Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
@@ -125,13 +129,17 @@ Include the `plyr.js` script before the closing `</body>` tag and then in your J
See [initialising](#initialising) for more information on advanced setups. See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following: 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 seperately as part of your application but to make life easier you can use the polyfilled build.
```html ```html
<script src="https://cdn.plyr.io/3.2.2/plyr.js"></script> <script src="https://cdn.plyr.io/3.3.12/plyr.js"></script>
``` ```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility ...or...
```html
<script src="https://cdn.plyr.io/3.3.12/plyr.polyfilled.js"></script>
```
### CSS ### CSS
@@ -144,13 +152,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.2.2/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.3.12/plyr.css">
``` ```
### SVG Sprite ### 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 The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.2.2/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.12/plyr.svg`.
## Ads ## Ads
@@ -210,7 +218,7 @@ You can specify a range of arguments for the constructor to use:
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList) * A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object * A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
Here's some examples Here's some examples
@@ -226,20 +234,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
const player = new Plyr(document.getElementById('player')); const player = new Plyr(document.getElementById('player'));
``` ```
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList): Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
```javascript ```javascript
const player = new Plyr(document.querySelectorAll('.js-player')); const player = new Plyr(document.querySelectorAll('.js-player'));
``` ```
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
##### Setting up multiple players ##### Setting up multiple players
You have two choices here. You can either use a simple array loop to map the constructor:
```javascript ```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player)); const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
``` ```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
```javascript
const players = Plyr.setup('.js-player');
```
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
##### Passing options
The second argument for the constructor is the [options](#options) object: The second argument for the constructor is the [options](#options) object:
```javascript ```javascript
@@ -248,7 +268,7 @@ const player = new Plyr('#player', {
}); });
``` ```
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info. In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
#### Options #### Options
@@ -279,7 +299,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. | | `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. | | `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. | | `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. | | `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally | | `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. | | `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. | | `duration` | Number | `null` | Specify a custom duration for media. |
@@ -287,7 +307,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. | | `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. | | `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. | | `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: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). | | `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 unselectable language options). |
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) | | `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. | | `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. | | `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
@@ -345,7 +365,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `fullscreen.exit()` | - | Exit fullscreen. | | `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. | | `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. | | `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. | | `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. | | `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. | | `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. | | `supports(type)` | String | Check support for a mime type. |
@@ -374,8 +394,9 @@ player.fullscreen.active; // false;
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. | | `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. | | `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. | | `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. | | `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered | | `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. | | `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
@@ -388,9 +409,10 @@ player.fullscreen.active; // false;
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. | | `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. | | `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. | | `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. | | `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. | | `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. | | `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. | | `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. | | `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. | | `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
@@ -590,15 +612,6 @@ document then the shortcuts will work when any element has focus, apart from an
| `C` | Toggle captions | | `C` | Toggle captions |
| `L` | Toggle loop | | `L` | Toggle loop |
## Streaming
Because Plyr is an extension of the standard HTML5 video and audio elements, third party streaming plugins can be used with Plyr. Massive thanks to Matias
Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's a few examples:
* Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB)
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
## Fullscreen ## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen). Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
@@ -607,19 +620,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers. Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported | | Browser | Supported |
| ------------- | --------- | | ------------- | ------------- |
| Safari | ✓ | | Safari | ✓ |
| Mobile Safari | ✓&sup1; | | Mobile Safari | ✓&sup1; |
| Firefox | ✓ | | Firefox | ✓ |
| Chrome | ✓ | | Chrome | ✓ |
| Opera | ✓ | | Opera | ✓ |
| Edge | ✓ | | Edge | ✓ |
| IE11 | ✓ | | IE11 | ✓&sup3; |
| IE10 | ✓&sup2; | | IE10 | ✓&sup2;&sup3; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide. 1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)) 2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
3. Polyfills required. See below.
### Polyfills ### Polyfills
@@ -668,8 +682,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
## Donate ## Donate
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated... Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
* [Donate via Patron](https://www.patreon.com/plyr)
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
## Mentions ## Mentions
@@ -707,10 +723,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
## Thanks ## Thanks
[![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/) [![Fastly](https://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services. Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Copyright and License ## Copyright and License
[The MIT license](license.md) [The MIT license](license.md)
+172 -152
View File
@@ -3,10 +3,10 @@
// TODO: Create as class // TODO: Create as class
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support';
import utils from './utils';
const captions = { const captions = {
// Setup captions // Setup captions
@@ -16,28 +16,6 @@ const captions = {
return; return;
} }
// Set default language if not set
const stored = this.storage.get('language');
if (!utils.is.empty(stored)) {
this.captions.language = stored;
}
if (utils.is.empty(this.captions.language)) {
this.captions.language = this.config.captions.language.toLowerCase();
}
// Set captions enabled state if not set
if (!utils.is.boolean(this.captions.active)) {
const active = this.storage.get('captions');
if (utils.is.boolean(active)) {
this.captions.active = active;
} else {
this.captions.active = this.config.captions.active;
}
}
// Only Vimeo and HTML5 video supported at this point // Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide // Clear menu and hide
@@ -55,17 +33,6 @@ const captions = {
utils.insertAfter(this.elements.captions, this.elements.wrapper); utils.insertAfter(this.elements.captions, this.elements.wrapper);
} }
// Set the class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
// Get tracks
const tracks = captions.getTracks.call(this);
// If no caption file exists, hide container for caption text
if (utils.is.empty(tracks)) {
return;
}
// Get browser info // Get browser info
const browser = utils.getBrowser(); const browser = utils.getBrowser();
@@ -94,82 +61,161 @@ const captions = {
}); });
} }
// Set language // Try to load the value from storage
captions.setLanguage.call(this); let active = this.storage.get('captions');
// Enable UI // Otherwise fall back to the default config
captions.show.call(this); if (!utils.is.boolean(active)) {
({ active } = this.config.captions);
}
// Set available languages in list // Get language from storage, fallback to config
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { let language = this.storage.get('language') || this.config.captions.language;
if (language === 'auto') {
[ language ] = (navigator.language || navigator.userLanguage).split('-');
}
// Set language and show if active
captions.setLanguage.call(this, language, active);
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { language, meta } = this.captions;
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks
.filter(track => !meta.get(track))
.forEach(track => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
});
}
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
const firstMatch = this.language !== language && tracks.find(track => track.language === language);
// Update language if removed or first matching track added
if (trackRemoved || firstMatch) {
captions.setLanguage.call(this, language, this.config.captions.active);
}
// Enable or disable captions based on track length
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks));
// Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this); controls.setCaptionsMenu.call(this);
} }
}, },
// Set the captions language set(index, setLanguage = true, show = true) {
setLanguage() { const tracks = captions.getTracks.call(this);
// Setup HTML5 track rendering
if (this.isHTML5 && this.isVideo) {
captions.getTracks.call(this).forEach(track => {
// Show track
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
// Turn off native caption rendering to avoid double captions // Disable captions if setting to -1
// eslint-disable-next-line if (index === -1) {
track.mode = 'hidden'; this.toggleCaptions(false);
}); return;
}
// Get current track if (!utils.is.number(index)) {
const currentTrack = captions.getCurrentTrack.call(this); this.debug.warn('Invalid caption argument', index);
return;
}
// Check if suported kind if (!(index in tracks)) {
if (utils.is.track(currentTrack)) { this.debug.warn('Track not found', index);
// If we change the active track while a cue is already displayed we need to update it return;
if (Array.from(currentTrack.activeCues || []).length) { }
captions.setCue.call(this, currentTrack);
} if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = captions.getCurrentTrack.call(this);
const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// Prevent setting language in some cases, since it can violate user's intentions
if (setLanguage) {
this.captions.language = language;
} }
} else if (this.isVimeo && this.captions.active) {
this.embed.enableTextTrack(this.language); // Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Trigger event
utils.dispatchEvent.call(this, this.media, 'languagechange');
}
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
// Show captions
if (show) {
this.toggleCaptions(true);
} }
}, },
// Get the tracks setLanguage(language, show = true) {
getTracks() { if (!utils.is.string(language)) {
// Return empty array at least this.debug.warn('Invalid language argument', language);
if (utils.is.nullOrUndefined(this.media)) { return;
return [];
} }
// Normalize
this.captions.language = language.toLowerCase();
// Only get accepted kinds // Set currentTrack
return Array.from(this.media.textTracks || []).filter(track => [ const tracks = captions.getTracks.call(this);
'captions', const track = captions.getCurrentTrack.call(this, true);
'subtitles', captions.set.call(this, tracks.indexOf(track), false, show);
].includes(track.kind)); },
// Get current valid caption tracks
// If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => [
'captions',
'subtitles',
].includes(track.kind));
}, },
// Get the current track for the current language // Get the current track for the current language
getCurrentTrack() { getCurrentTrack(fromLanguage = false) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
if (!tracks.length) { const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
return null; return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0];
}
// Get track based on current language
let track = tracks.find(track => track.language.toLowerCase() === this.language);
// Get the <track> with default attribute
if (!track) {
track = utils.getElement.call(this, 'track[default]');
}
// Get the first track
if (!track) {
[track] = tracks;
}
return track;
}, },
// Get UI label for track // Get UI label for track
@@ -195,74 +241,48 @@ const captions = {
return i18n.get('disabled', this.config); return i18n.get('disabled', this.config);
}, },
// Display active caption if it contains text // Update captions using current track's active cues
setCue(input) { // Also optional array argument in case there isn't any track (ex: vimeo)
// Get the track from the event if needed updateCues(input) {
const track = utils.is.event(input) ? input.target : input;
const { activeCues } = track;
const active = activeCues.length && activeCues[0];
const currentTrack = captions.getCurrentTrack.call(this);
// Only display current track
if (track !== currentTrack) {
return;
}
// Display a cue, if there is one
if (utils.is.cue(active)) {
captions.setText.call(this, active.getCueAsHTML());
} else {
captions.setText.call(this, null);
}
utils.dispatchEvent.call(this, this.media, 'cuechange');
},
// Set the current caption
setText(input) {
// Requires UI // Requires UI
if (!this.supported.ui) { if (!this.supported.ui) {
return; return;
} }
if (utils.is.element(this.elements.captions)) { if (!utils.is.element(this.elements.captions)) {
const content = utils.createElement('span');
// Empty the container
utils.emptyElement(this.elements.captions);
// Default to empty
const caption = !utils.is.nullOrUndefined(input) ? input : '';
// Set the span content
if (utils.is.string(caption)) {
content.textContent = caption.trim();
} else {
content.appendChild(caption);
}
// Set new caption text
this.elements.captions.appendChild(content);
} else {
this.debug.warn('No captions element to render to'); this.debug.warn('No captions element to render to');
} return;
},
// Display captions container and button (for initialization)
show() {
// Try to load the value from storage
let active = this.storage.get('captions');
// Otherwise fall back to the default config
if (!utils.is.boolean(active)) {
({ active } = this.config.captions);
} else {
this.captions.active = active;
} }
if (active) { // Only accept array or empty input
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) {
utils.toggleState(this.elements.buttons.captions, true); this.debug.warn('updateCues: Invalid input', input);
return;
}
let cues = input;
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(utils.getHTML);
}
// Set new caption text
const content = cues.map(cueText => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
if (changed) {
// Empty the container and create a new child element
utils.emptyElement(this.elements.captions);
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
utils.dispatchEvent.call(this, this.media, 'cuechange');
} }
}, },
}; };
+315 -120
View File
@@ -2,52 +2,88 @@
// Plyr controls // Plyr controls
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions'; import captions from './captions';
import html5 from './html5'; import html5 from './html5';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser // Sniff out the browser
const browser = utils.getBrowser(); const browser = utils.getBrowser();
const controls = { const controls = {
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// Get range from event if event passed
const range = utils.is.event(target) ? target.target : target;
// Needs to be a valid <input type='range'>
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// WebKit only
if (!browser.isWebkit) {
return;
}
// Set CSS custom property
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
},
// Get icon URL // Get icon URL
getIconUrl() { getIconUrl() {
const url = new URL(this.config.iconUrl, window.location);
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
return { return {
url: this.config.iconUrl, url: this.config.iconUrl,
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody), cors,
}; };
}, },
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Create <svg> icon // Create <svg> icon
createIcon(type, attributes) { createIcon(type, attributes) {
const namespace = 'http://www.w3.org/2000/svg'; const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this); const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`; const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg> // Create <svg>
const icon = document.createElementNS(namespace, 'svg'); const icon = document.createElementNS(namespace, 'svg');
@@ -210,7 +246,6 @@ const controls = {
// Add aria attributes // Add aria attributes
attributes['aria-pressed'] = false; attributes['aria-pressed'] = false;
attributes['aria-label'] = i18n.get(label, this.config);
} else { } else {
button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label)); button.appendChild(controls.createLabel.call(this, label));
@@ -317,7 +352,7 @@ const controls = {
break; break;
} }
progress.textContent = `% ${suffix.toLowerCase()}`; progress.innerText = `% ${suffix.toLowerCase()}`;
} }
this.elements.display[type] = progress; this.elements.display[type] = progress;
@@ -327,29 +362,21 @@ const controls = {
// Create time display // Create time display
createTime(type) { createTime(type) {
const container = utils.createElement('div', { const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
class: 'plyr__time',
});
container.appendChild( const container = utils.createElement('div', utils.extend(attributes, {
utils.createElement( class: `plyr__time ${attributes.class}`,
'span', 'aria-label': i18n.get(type, this.config),
{ }), '00:00');
class: this.config.classNames.hidden,
},
i18n.get(type, this.config),
),
);
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
// Reference for updates
this.elements.display[type] = container; this.elements.display[type] = container;
return container; return container;
}, },
// Create a settings menu item // Create a settings menu item
createMenuItem(value, list, type, title, badge = null, checked = false) { createMenuItem({value, list, type, title, badge = null, checked = false}) {
const item = utils.createElement('li'); const item = utils.createElement('li');
const label = utils.createElement('label', { const label = utils.createElement('label', {
@@ -381,6 +408,124 @@ const controls = {
list.appendChild(item); list.appendChild(item);
}, },
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target) || !utils.is.number(time)) {
return;
}
// Always display hours if duration is over an hour
const forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.innerText = utils.formatTime(time, forceHours, inverted);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.element(this.elements.inputs.volume)) {
controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
return;
}
// eslint-disable-next-line
target.value = value;
// Webkit range fill
controls.updateRangeFill.call(this, target);
},
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
let value = 0;
const setProgress = (target, input) => {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
};
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
case 'seeked':
value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event
if (event.type === 'timeupdate') {
controls.setRange.call(this, this.elements.inputs.seek, value);
}
break;
// Check buffer status
case 'playing':
case 'progress':
setProgress(this.elements.display.buffer, this.buffered * 100);
break;
default:
break;
}
}
},
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// Get range from event if event passed
const range = utils.is.event(target) ? target.target : target;
// Needs to be a valid <input type='range'>
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// WebKit only
if (!browser.isWebkit) {
return;
}
// Set CSS custom property
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
},
// Update hover tooltip for seeking // Update hover tooltip for seeking
updateSeekTooltip(event) { updateSeekTooltip(event) {
// Bail if setting not true // Bail if setting not true
@@ -395,7 +540,7 @@ const controls = {
// Calculate percentage // Calculate percentage
let percent = 0; let percent = 0;
const clientRect = this.elements.inputs.seek.getBoundingClientRect(); const clientRect = this.elements.progress.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`; const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => { const toggle = toggle => {
@@ -425,7 +570,7 @@ const controls = {
} }
// Display the time a click would seek to // Display the time a click would seek to
ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
// Set position // Set position
this.elements.display.seekTooltip.style.left = `${percent}%`; this.elements.display.seekTooltip.style.left = `${percent}%`;
@@ -440,6 +585,47 @@ const controls = {
} }
}, },
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// Playing progress
controls.updateProgress.call(this, event);
},
// Show the duration on metadataloaded or durationchange events
durationUpdate() {
// Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {
return;
}
// If there's a spot to display duration
const hasDuration = utils.is.element(this.elements.display.duration);
// If there's only one time display, display duration there
if (!hasDuration && this.config.displayDuration && this.paused) {
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (hasDuration) {
controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
// Hide/show a tab // Hide/show a tab
toggleTab(setting, toggle) { toggleTab(setting, toggle) {
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle); utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
@@ -478,26 +664,7 @@ const controls = {
// Get the badge HTML for HD, 4K etc // Get the badge HTML for HD, 4K etc
const getBadge = quality => { const getBadge = quality => {
let label = ''; const label = i18n.get(`qualityBadge.${quality}`, this.config);
switch (quality) {
case 2160:
label = '4K';
break;
case 1440:
case 1080:
case 720:
label = 'HD';
break;
case 576:
label = 'SD';
break;
default:
break;
}
if (!label.length) { if (!label.length) {
return null; return null;
@@ -513,15 +680,19 @@ const controls = {
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
}) })
.forEach(quality => { .forEach(quality => {
const label = controls.getLabel.call(this, 'quality', quality); controls.createMenuItem.call(this, {
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality)); value: quality,
list,
type,
title: controls.getLabel.call(this, 'quality', quality),
badge: getBadge(quality),
});
}); });
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
// Translate a value into a nice label // Translate a value into a nice label
// TODO: Localisation
getLabel(setting, value) { getLabel(setting, value) {
switch (setting) { switch (setting) {
case 'speed': case 'speed':
@@ -529,7 +700,13 @@ const controls = {
case 'quality': case 'quality':
if (utils.is.number(value)) { if (utils.is.number(value)) {
return `${value}p`; const label = i18n.get(`qualityLabel.${value}`, this.config);
if (!label.length) {
return `${value}p`;
}
return label;
} }
return utils.toTitleCase(value); return utils.toTitleCase(value);
@@ -550,16 +727,7 @@ const controls = {
switch (setting) { switch (setting) {
case 'captions': case 'captions':
if (this.captions.active) { value = this.currentTrack;
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
value = this.captions.language;
} else {
value = 'enabled';
}
} else {
value = '';
}
break; break;
default: default:
@@ -659,10 +827,10 @@ const controls = {
// TODO: Captions or language? Currently it's mixed // TODO: Captions or language? Currently it's mixed
const type = 'captions'; const type = 'captions';
const list = this.elements.settings.panes.captions.querySelector('ul'); const list = this.elements.settings.panes.captions.querySelector('ul');
const tracks = captions.getTracks.call(this);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = captions.getTracks.call(this).length; controls.toggleTab.call(this, type, tracks.length);
controls.toggleTab.call(this, type, toggle);
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
@@ -671,37 +839,31 @@ const controls = {
controls.checkMenu.call(this); controls.checkMenu.call(this);
// If there's no captions, bail // If there's no captions, bail
if (!toggle) { if (!tracks.length) {
return; return;
} }
// Re-map the tracks into just the data we need // Generate options data
const tracks = captions.getTracks.call(this).map(track => ({ const options = tracks.map((track, value) => ({
language: !utils.is.empty(track.language) ? track.language : 'enabled', value,
label: captions.getLabel.call(this, track), checked: this.captions.active && this.currentTrack === value,
title: captions.getLabel.call(this, track),
badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),
list,
type: 'language',
})); }));
// Add the "Disabled" option to turn off captions // Add the "Disabled" option to turn off captions
tracks.unshift({ options.unshift({
language: '', value: -1,
label: i18n.get('disabled', this.config), checked: !this.captions.active,
title: i18n.get('disabled', this.config),
list,
type: 'language',
}); });
// Generate options // Generate options
tracks.forEach(track => { options.forEach(controls.createMenuItem.bind(this));
controls.createMenuItem.call(
this,
track.language,
list,
'language',
track.label,
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
track.language.toLowerCase() === this.captions.language.toLowerCase(),
);
});
// Store reference
this.options.captions = tracks.map(track => track.language);
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
@@ -758,8 +920,12 @@ const controls = {
// Create items // Create items
this.options.speed.forEach(speed => { this.options.speed.forEach(speed => {
const label = controls.getLabel.call(this, 'speed', speed); controls.createMenuItem.call(this, {
controls.createMenuItem.call(this, speed, list, type, label); value: speed,
list,
type,
title: controls.getLabel.call(this, 'speed', speed),
});
}); });
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
@@ -849,11 +1015,9 @@ const controls = {
}, },
// Toggle Menu // Toggle Menu
showTab(event) { showTab(target = '') {
const { menu } = this.elements.settings; const { menu } = this.elements.settings;
const tab = event.target; const pane = document.getElementById(target);
const show = tab.getAttribute('aria-expanded') === 'false';
const pane = document.getElementById(tab.getAttribute('aria-controls'));
// Nothing to show, bail // Nothing to show, bail
if (!utils.is.element(pane)) { if (!utils.is.element(pane)) {
@@ -916,8 +1080,12 @@ const controls = {
current.setAttribute('tabindex', -1); current.setAttribute('tabindex', -1);
// Set attributes on target // Set attributes on target
utils.toggleHidden(pane, !show); utils.toggleHidden(pane, false);
tab.setAttribute('aria-expanded', show);
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true);
});
pane.removeAttribute('tabindex'); pane.removeAttribute('tabindex');
// Focus the first item // Focus the first item
@@ -976,7 +1144,6 @@ const controls = {
const tooltip = utils.createElement( const tooltip = utils.createElement(
'span', 'span',
{ {
role: 'tooltip',
class: this.config.classNames.tooltip, class: this.config.classNames.tooltip,
}, },
'00:00', '00:00',
@@ -1192,7 +1359,7 @@ const controls = {
const icon = controls.getIconUrl.call(this); const icon = controls.getIconUrl.call(this);
// Only load external sprite using AJAX // Only load external sprite using AJAX
if (icon.absolute) { if (icon.cors) {
utils.loadSprite(icon.url, 'sprite-plyr'); utils.loadSprite(icon.url, 'sprite-plyr');
} }
} }
@@ -1204,17 +1371,21 @@ const controls = {
let container = null; let container = null;
this.elements.controls = null; this.elements.controls = null;
// HTML or Element passed as the option // Set template properties
const props = {
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
};
let update = true;
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) { if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
// String or HTMLElement passed as the option
container = this.config.controls; container = this.config.controls;
} else if (utils.is.function(this.config.controls)) { } else if (utils.is.function(this.config.controls)) {
// A custom function to build controls // A custom function to build controls
// The function can return a HTMLElement or String // The function can return a HTMLElement or String
container = this.config.controls({ container = this.config.controls.call(this, props);
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
});
} else { } else {
// Create controls // Create controls
container = controls.create.call(this, { container = controls.create.call(this, {
@@ -1226,6 +1397,30 @@ const controls = {
// TODO: Looping // TODO: Looping
// loop: 'None', // loop: 'None',
}); });
update = false;
}
// Replace props with their value
const replace = input => {
let result = input;
Object.entries(props).forEach(([
key,
value,
]) => {
result = utils.replaceAll(result, `{${key}}`, value);
});
return result;
};
// Update markup
if (update) {
if (utils.is.string(this.config.controls)) {
container = replace(container);
} else if (utils.is.element(container)) {
container.innerHTML = replace(container.innerHTML);
}
} }
// Controls container // Controls container
@@ -1250,7 +1445,7 @@ const controls = {
// Find the elements if need be // Find the elements if need be
if (!utils.is.element(this.elements.controls)) { if (!utils.is.element(this.elements.controls)) {
utils.findElements.call(this); controls.findElements.call(this);
} }
// Edge sometimes doesn't finish the paint so force a redraw // Edge sometimes doesn't finish the paint so force a redraw
+35 -18
View File
@@ -47,8 +47,8 @@ const defaults = {
// Auto hide the controls // Auto hide the controls
hideControls: true, hideControls: true,
// Revert to poster on finish (HTML5 - will cause reload) // Reset to start when playback ended
showPosterOnEnd: false, resetOnEnd: false,
// Disable the standard context menu // Disable the standard context menu
disableContextMenu: true, disableContextMenu: true,
@@ -56,7 +56,7 @@ const defaults = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.2.2/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -115,7 +115,10 @@ const defaults = {
// Captions settings // Captions settings
captions: { captions: {
active: false, active: false,
language: (navigator.language || navigator.userLanguage).split('-')[0], language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
}, },
// Fullscreen settings // Fullscreen settings
@@ -157,10 +160,10 @@ const defaults = {
// Localisation // Localisation
i18n: { i18n: {
restart: 'Restart', restart: 'Restart',
rewind: 'Rewind {seektime} secs', rewind: 'Rewind {seektime}s',
play: 'Play', play: 'Play',
pause: 'Pause', pause: 'Pause',
fastForward: 'Forward {seektime} secs', fastForward: 'Forward {seektime}s',
seek: 'Seek', seek: 'Seek',
played: 'Played', played: 'Played',
buffered: 'Buffered', buffered: 'Buffered',
@@ -187,18 +190,29 @@ const defaults = {
disabled: 'Disabled', disabled: 'Disabled',
enabled: 'Enabled', enabled: 'Enabled',
advertisement: 'Ad', advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
}, },
// URLs // URLs
urls: { urls: {
vimeo: { vimeo: {
api: 'https://player.vimeo.com/api/player.js', sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
}, },
youtube: { youtube: {
api: 'https://www.youtube.com/iframe_api', sdk: 'https://www.youtube.com/iframe_api',
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
}, },
googleIMA: { googleIMA: {
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
}, },
}, },
@@ -308,13 +322,13 @@ const defaults = {
display: { display: {
currentTime: '.plyr__time--current', currentTime: '.plyr__time--current',
duration: '.plyr__time--duration', duration: '.plyr__time--duration',
buffer: '.plyr__progress--buffer', buffer: '.plyr__progress__buffer',
played: '.plyr__progress--played', loop: '.plyr__progress__loop', // Used later
loop: '.plyr__progress--loop',
volume: '.plyr__volume--display', volume: '.plyr__volume--display',
}, },
progress: '.plyr__progress', progress: '.plyr__progress',
captions: '.plyr__captions', captions: '.plyr__captions',
caption: '.plyr__caption',
menu: { menu: {
quality: '.js-plyr__menu__list--quality', quality: '.js-plyr__menu__list--quality',
}, },
@@ -322,16 +336,19 @@ const defaults = {
// Class hooks added to the player in different states // Class hooks added to the player in different states
classNames: { classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}', type: 'plyr--{0}',
provider: 'plyr--{0}', provider: 'plyr--{0}',
stopped: 'plyr--stopped', video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
playing: 'plyr--playing', playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading', loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover', hover: 'plyr--hover',
tooltip: 'plyr__tooltip', tooltip: 'plyr__tooltip',
cues: 'plyr__cues', cues: 'plyr__cues',
+7 -7
View File
@@ -19,7 +19,7 @@ function onChange() {
} }
// Trigger an event // Trigger an event
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container // Trap focus in container
if (!browser.isIos) { if (!browser.isIos) {
@@ -55,7 +55,7 @@ class Fullscreen {
// Get prefix // Get prefix
this.prefix = Fullscreen.prefix; this.prefix = Fullscreen.prefix;
this.name = Fullscreen.name; this.property = Fullscreen.property;
// Scroll position // Scroll position
this.scrollPosition = { x: 0, y: 0 }; this.scrollPosition = { x: 0, y: 0 };
@@ -70,7 +70,7 @@ class Fullscreen {
// Fullscreen toggle on double click // Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => { utils.on(this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls // Ignore double click in controls
if (this.player.elements.controls.contains(event.target)) { if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return; return;
} }
@@ -113,7 +113,7 @@ class Fullscreen {
return value; return value;
} }
static get name() { static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
} }
@@ -138,7 +138,7 @@ class Fullscreen {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
} }
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`]; const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target; return element === this.target;
} }
@@ -176,7 +176,7 @@ class Fullscreen {
} else if (!this.prefix) { } else if (!this.prefix) {
this.target.requestFullscreen(); this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) { } else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.name}`](); this.target[`${this.prefix}Request${this.property}`]();
} }
} }
@@ -196,7 +196,7 @@ class Fullscreen {
(document.cancelFullScreen || document.exitFullscreen).call(document); (document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) { } else if (!utils.is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.name}`](); document[`${this.prefix}${action}${this.property}`]();
} }
} }
+7 -3
View File
@@ -99,6 +99,13 @@ const html5 = {
// Set new source // Set new source
player.media.src = supported[0].getAttribute('src'); player.media.src = supported[0].getAttribute('src');
// Restore time
const onLoadedMetaData = () => {
player.currentTime = currentTime;
player.off('loadedmetadata', onLoadedMetaData);
};
player.on('loadedmetadata', onLoadedMetaData);
// Load new source // Load new source
player.media.load(); player.media.load();
@@ -107,9 +114,6 @@ const html5 = {
player.play(); player.play();
} }
// Restore time
player.currentTime = currentTime;
// Trigger change event // Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
quality: input, quality: input,
+6 -2
View File
@@ -6,11 +6,15 @@ import utils from './utils';
const i18n = { const i18n = {
get(key = '', config = {}) { get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) { if (utils.is.empty(key) || utils.is.empty(config)) {
return ''; return '';
} }
let string = config.i18n[key]; let string = utils.getDeep(config.i18n, key);
if (utils.is.empty(string)) {
return '';
}
const replace = { const replace = {
'{seektime}': config.seekTime, '{seektime}': config.seekTime,
+142 -44
View File
@@ -2,9 +2,9 @@
// Plyr Event Listeners // Plyr Event Listeners
// ========================================================================== // ==========================================================================
import utils from './utils';
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import utils from './utils';
// Sniff out the browser // Sniff out the browser
const browser = utils.getBrowser(); const browser = utils.getBrowser();
@@ -74,7 +74,10 @@ class Listeners {
// and if the focused element is not editable (e.g. text input) // and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/ // and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement(); const focused = utils.getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) { if (utils.is.element(focused) && (
focused !== this.player.elements.inputs.seek &&
utils.matches(focused, this.player.config.selectors.editable))
) {
return; return;
} }
@@ -238,22 +241,45 @@ class Listeners {
}, 0); }, 0);
}); });
// Toggle controls visibility based on mouse movement // Toggle controls on mouse events and entering fullscreen
if (this.player.config.hideControls) { utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
// Toggle controls on mouse events and entering fullscreen const { controls } = this.player.elements;
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
this.player.toggleControls(event); // Remove button states for fullscreen
}); if (event.type === 'enterfullscreen') {
} controls.pressed = false;
controls.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
const show = [
'touchstart',
'touchmove',
'mousemove',
].includes(event.type);
let delay = 0;
if (show) {
ui.toggleControls.call(this.player, true);
// Use longer timeout for touch devices
delay = this.player.touch ? 3000 : 2000;
}
// Clear timer
clearTimeout(this.player.timers.controls);
// Timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
});
} }
// Listen for media events // Listen for media events
media() { media() {
// Time change on media // Time change on media
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event)); utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
// Display duration // Display duration
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event)); utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
@@ -265,30 +291,24 @@ class Listeners {
// Handle the media finishing // Handle the media finishing
utils.on(this.player.media, 'ended', () => { utils.on(this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) { if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart // Restart
this.player.restart(); this.player.restart();
// Re-load media
this.player.media.load();
} }
}); });
// Check for buffer progress // Check for buffer progress
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event)); utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
// Handle volume changes // Handle volume changes
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event)); utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
// Handle play/pause // Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event)); utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
// Loading state // Loading state
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
// Check if media failed to load
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
// If autoplay, then load advertisement if required // If autoplay, then load advertisement if required
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
utils.on(this.player.media, 'playing', () => { utils.on(this.player.media, 'playing', () => {
@@ -394,7 +414,7 @@ class Listeners {
'keyup', 'keyup',
'keydown', 'keydown',
]).join(' '), event => { ]).join(' '), event => {
let detail = {}; let {detail = {}} = event;
// Get error details from media // Get error details from media
if (event.type === 'error') { if (event.type === 'error') {
@@ -492,12 +512,19 @@ class Listeners {
on(this.player.elements.settings.form, 'click', event => { on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click
const showHomeTab = () => {
const id = `plyr-settings-${this.player.id}-home`;
controls.showTab.call(this.player, id);
};
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) { if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy( proxy(
event, event,
() => { () => {
this.player.language = event.target.value; this.player.currentTrack = Number(event.target.value);
showHomeTab();
}, },
'language', 'language',
); );
@@ -506,6 +533,7 @@ class Listeners {
event, event,
() => { () => {
this.player.quality = event.target.value; this.player.quality = event.target.value;
showHomeTab();
}, },
'quality', 'quality',
); );
@@ -514,11 +542,50 @@ class Listeners {
event, event,
() => { () => {
this.player.speed = parseFloat(event.target.value); this.player.speed = parseFloat(event.target.value);
showHomeTab();
}, },
'speed', 'speed',
); );
} else { } else {
controls.showTab.call(this.player, event); const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
}
});
// Set range input alternative "value", which matches the tooltip time (#954)
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
const clientRect = this.player.elements.progress.getBoundingClientRect();
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seek-value', percent);
});
// Pause while seeking
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which;
const eventType = event.type;
if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) {
return;
}
// Was playing before?
const play = seek.hasAttribute('play-on-seeked');
// Done seeking
const done = [
'mouseup',
'touchend',
'keyup',
].includes(event.type);
// If we're done seeking and it was playing, resume playback
if (play && done) {
seek.removeAttribute('play-on-seeked');
this.player.play();
} else if (!done && this.player.playing) {
seek.setAttribute('play-on-seeked', '');
this.player.pause();
} }
}); });
@@ -527,7 +594,18 @@ class Listeners {
this.player.elements.inputs.seek, this.player.elements.inputs.seek,
inputEvent, inputEvent,
event => { event => {
this.player.currentTime = event.target.value / event.target.max * this.player.duration; const seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
if (utils.is.empty(seekTo)) {
seekTo = seek.value;
}
seek.removeAttribute('seek-value');
this.player.currentTime = seekTo / seek.max * this.player.duration;
}, },
'seek', 'seek',
); );
@@ -542,7 +620,8 @@ class Listeners {
} }
this.player.config.invertTime = !this.player.config.invertTime; this.player.config.invertTime = !this.player.config.invertTime;
ui.timeUpdate.call(this.player);
controls.timeUpdate.call(this.player);
}); });
} }
@@ -566,26 +645,45 @@ class Listeners {
// Seek tooltip // Seek tooltip
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event)); on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
// Toggle controls visibility based on mouse movement // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
if (this.player.config.hideControls) { on(this.player.elements.controls, 'mouseenter mouseleave', event => {
// Watch for cursor over controls so they don't hide when trying to interact this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
on(this.player.elements.controls, 'mouseenter mouseleave', event => { });
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
});
// Watch for cursor over controls so they don't hide when trying to interact // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = [ this.player.elements.controls.pressed = [
'mousedown', 'mousedown',
'touchstart', 'touchstart',
].includes(event.type); ].includes(event.type);
}); });
// Focus in/out on controls // Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', event => { on(this.player.elements.controls, 'focusin focusout', event => {
this.player.toggleControls(event); const { config, elements, timers } = this.player;
});
} // Skip transition to prevent focus from scrolling the parent element
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
// Toggle
ui.toggleControls.call(this.player, event.type === 'focusin');
// If focusin, hide again after delay
if (event.type === 'focusin') {
// Restore transition
setTimeout(() => {
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0);
// Delay a little more for keyboard users
const delay = this.touch ? 3000 : 4000;
// Clear timer
clearTimeout(timers.controls);
// Hide
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
}
});
// Mouse wheel for volume // Mouse wheel for volume
on( on(
+10 -27
View File
@@ -2,15 +2,10 @@
// Plyr Media // Plyr Media
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo';
import html5 from './html5'; import html5 from './html5';
import ui from './ui'; import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
// Sniff out the browser import utils from './utils';
const browser = utils.getBrowser();
const media = { const media = {
// Setup media // Setup media
@@ -33,23 +28,6 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
} }
if (this.supported.ui) {
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// If there's no autoplay attribute, assume the video is stopped and add state class
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
}
// Inject the player wrapper // Inject the player wrapper
if (this.isVideo) { if (this.isVideo) {
// Create the wrapper div // Create the wrapper div
@@ -59,6 +37,13 @@ const media = {
// Wrap the video in a container // Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper); utils.wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
} }
if (this.isEmbed) { if (this.isEmbed) {
@@ -75,8 +60,6 @@ const media = {
break; break;
} }
} else if (this.isHTML5) { } else if (this.isHTML5) {
ui.setTitle.call(this);
html5.extend.call(this); html5.extend.call(this);
} }
}, },
+10 -4
View File
@@ -6,8 +6,8 @@
/* global google */ /* global google */
import utils from '../utils';
import i18n from '../i18n'; import i18n from '../i18n';
import utils from '../utils';
class Ads { class Ads {
/** /**
@@ -18,7 +18,6 @@ class Ads {
constructor(player) { constructor(player) {
this.player = player; this.player = player;
this.publisherId = player.config.ads.publisherId; this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false; this.playing = false;
this.initialized = false; this.initialized = false;
this.elements = { this.elements = {
@@ -44,6 +43,10 @@ class Ads {
this.load(); this.load();
} }
get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
}
/** /**
* Load the IMA SDK * Load the IMA SDK
*/ */
@@ -52,7 +55,7 @@ class Ads {
// Check if the Google IMA3 SDK is loaded or load it ourselves // Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) { if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils utils
.loadScript(this.player.config.urls.googleIMA.api) .loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => { .then(() => {
this.ready(); this.ready();
}) })
@@ -160,6 +163,9 @@ class Ads {
// We only overlay ads as we only support video. // We only overlay ads as we only support video.
request.forceNonLinearFullSlot = false; request.forceNonLinearFullSlot = false;
// Mute based on current state
request.setAdWillPlayMuted(!this.player.muted);
this.loader.requestAds(request); this.loader.requestAds(request);
} catch (e) { } catch (e) {
this.onAdError(e); this.onAdError(e);
@@ -226,7 +232,7 @@ class Ads {
// Get skippable state // Get skippable state
// TODO: Skip button // TODO: Skip button
// this.manager.getAdSkippableState(); // this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player // Set volume to match player
this.manager.setVolume(this.player.volume); this.manager.setVolume(this.player.volume);
+72 -44
View File
@@ -2,10 +2,21 @@
// Vimeo plugin // Vimeo plugin
// ========================================================================== // ==========================================================================
import utils from './../utils';
import captions from './../captions'; import captions from './../captions';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils';
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = { const vimeo = {
setup() { setup() {
@@ -18,7 +29,7 @@ const vimeo = {
// Load the API if not already // Load the API if not already
if (!utils.is.object(window.Vimeo)) { if (!utils.is.object(window.Vimeo)) {
utils utils
.loadScript(this.config.urls.vimeo.api) .loadScript(this.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(this); vimeo.ready.call(this);
}) })
@@ -53,6 +64,7 @@ const vimeo = {
const options = { const options = {
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
// muted: player.muted,
byline: false, byline: false,
portrait: false, portrait: false,
title: false, title: false,
@@ -68,27 +80,46 @@ const vimeo = {
// Get from <div> if needed // Get from <div> if needed
if (utils.is.empty(source)) { if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id); source = player.media.getAttribute(player.config.attributes.embed.id);
} }
const id = utils.parseVimeoId(source); const id = utils.parseVimeoId(source);
// Build an iframe // Build an iframe
const iframe = utils.createElement('iframe'); const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${id}?${params}`; const src = utils.format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay'); iframe.setAttribute('allow', 'autoplay');
// Inject the package // Inject the package
const wrapper = utils.createElement('div'); const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe); wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media); player.media = utils.replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster
ui.setPoster.call(player, url.href);
});
// Setup instance // Setup instance
// https://github.com/vimeo/player.js // https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe); player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
player.media.paused = true; player.media.paused = true;
player.media.currentTime = 0; player.media.currentTime = 0;
@@ -100,15 +131,13 @@ const vimeo = {
// Create a faux HTML5 API using the Vimeo API // Create a faux HTML5 API using the Vimeo API
player.media.play = () => { player.media.play = () => {
player.embed.play().then(() => { assurePlaybackState.call(player, true);
player.media.paused = false; return player.embed.play();
});
}; };
player.media.pause = () => { player.media.pause = () => {
player.embed.pause().then(() => { assurePlaybackState.call(player, false);
player.media.paused = true; return player.embed.pause();
});
}; };
player.media.stop = () => { player.media.stop = () => {
@@ -123,25 +152,27 @@ const vimeo = {
return currentTime; return currentTime;
}, },
set(time) { set(time) {
// Get current paused state // Vimeo will automatically play on seek if the video hasn't been played before
// Vimeo will automatically play on seek
const { paused } = player.media;
// Set seeking flag // Get current paused state and volume etc
player.media.seeking = true; const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Trigger seeking // Set seeking state and trigger event
utils.dispatchEvent.call(player, player.media, 'seeking'); media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking');
// Seek after events // If paused, mute until seek is complete
player.embed.setCurrentTime(time).catch(() => { Promise.resolve(restorePause && embed.setVolume(0))
// Do nothing // Seek
}); .then(() => embed.setCurrentTime(time))
// Restore paused
// Restore pause state .then(() => restorePause && embed.pause())
if (paused) { // Restore volume
player.pause(); .then(() => restorePause && embed.setVolume(volume))
} .catch(() => {
// Do nothing
});
}, },
}); });
@@ -274,17 +305,20 @@ const vimeo = {
captions.setup.call(player); captions.setup.call(player);
}); });
player.embed.on('cuechange', data => { player.embed.on('cuechange', ({ cues = [] }) => {
let cue = null; const strippedCues = cues.map(cue => utils.stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
if (data.cues.length) {
cue = utils.stripHTML(data.cues[0].text);
}
captions.setText.call(player, cue);
}); });
player.embed.on('loaded', () => { player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused);
if (!paused) {
utils.dispatchEvent.call(player, player.media, 'playing');
}
});
if (utils.is.element(player.embed.element) && player.supported.ui) { if (utils.is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element; const frame = player.embed.element;
@@ -295,17 +329,12 @@ const vimeo = {
}); });
player.embed.on('play', () => { player.embed.on('play', () => {
// Only fire play if paused before assurePlaybackState.call(player, true);
if (player.media.paused) {
utils.dispatchEvent.call(player, player.media, 'play');
}
player.media.paused = false;
utils.dispatchEvent.call(player, player.media, 'playing'); utils.dispatchEvent.call(player, player.media, 'playing');
}); });
player.embed.on('pause', () => { player.embed.on('pause', () => {
player.media.paused = true; assurePlaybackState.call(player, false);
utils.dispatchEvent.call(player, player.media, 'pause');
}); });
player.embed.on('timeupdate', data => { player.embed.on('timeupdate', data => {
@@ -336,7 +365,6 @@ const vimeo = {
player.embed.on('seeked', () => { player.embed.on('seeked', () => {
player.media.seeking = false; player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked'); utils.dispatchEvent.call(player, player.media, 'seeked');
utils.dispatchEvent.call(player, player.media, 'play');
}); });
player.embed.on('ended', () => { player.embed.on('ended', () => {
+76 -45
View File
@@ -2,9 +2,9 @@
// YouTube plugin // YouTube plugin
// ========================================================================== // ==========================================================================
import utils from './../utils';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils';
// Standardise YouTube quality unit // Standardise YouTube quality unit
function mapQualityUnit(input) { function mapQualityUnit(input) {
@@ -64,6 +64,17 @@ function mapQualityUnits(levels) {
return utils.dedupe(levels.map(level => mapQualityUnit(level))); return utils.dedupe(levels.map(level => mapQualityUnit(level)));
} }
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
@@ -77,7 +88,7 @@ const youtube = {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Load the API // Load the API
utils.loadScript(this.config.urls.youtube.api).catch(error => { utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error); this.debug.warn('YouTube API failed to load', error);
}); });
@@ -117,7 +128,7 @@ const youtube = {
// Or via Google API // Or via Google API
const key = this.config.keys.google; const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) { if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`; const url = utils.format(this.config.urls.youtube.api, videoId, key);
utils utils
.fetch(url) .fetch(url)
@@ -161,6 +172,21 @@ const youtube = {
const container = utils.createElement('div', { id }); const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media); player.media = utils.replaceElement(container, player.media);
// Set poster image
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
});
// Setup instance // Setup instance
// https://developers.google.com/youtube/iframe_api_reference // https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, { player.embed = new window.YT.Player(id, {
@@ -249,10 +275,12 @@ const youtube = {
// Create a faux HTML5 API using the YouTube API // Create a faux HTML5 API using the YouTube API
player.media.play = () => { player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo(); instance.playVideo();
}; };
player.media.pause = () => { player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo(); instance.pauseVideo();
}; };
@@ -270,22 +298,17 @@ const youtube = {
return Number(instance.getCurrentTime()); return Number(instance.getCurrentTime());
}, },
set(time) { set(time) {
// Vimeo will automatically play on seek // If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
const { paused } = player.media; if (player.paused) {
player.embed.mute();
}
// Set seeking flag // Set seeking state and trigger event
player.media.seeking = true; player.media.seeking = true;
// Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking'); utils.dispatchEvent.call(player, player.media, 'seeking');
// Seek after events sent // Seek after events sent
instance.seekTo(time); instance.seekTo(time);
// Restore pause state
if (paused) {
player.pause();
}
}, },
}); });
@@ -404,6 +427,17 @@ const youtube = {
// Reset timer // Reset timer
clearInterval(player.timers.playing); clearInterval(player.timers.playing);
const seeked = player.media.seeking && [
1,
2,
].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
}
// Handle events // Handle events
// -1 Unstarted // -1 Unstarted
// 0 Ended // 0 Ended
@@ -423,7 +457,7 @@ const youtube = {
break; break;
case 0: case 0:
player.media.paused = true; assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it. // YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) { if (player.media.loop) {
@@ -437,42 +471,39 @@ const youtube = {
break; break;
case 1: case 1:
// If we were seeking, fire seeked event // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.seeking) { if (player.media.paused && !player.embed.hasPlayed) {
utils.dispatchEvent.call(player, player.media, 'seeked'); player.media.pause();
} else {
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange');
}
// Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
} }
player.media.seeking = false;
// Only fire play if paused before
if (player.media.paused) {
utils.dispatchEvent.call(player, player.media, 'play');
}
player.media.paused = false;
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange');
}
// Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
break; break;
case 2: case 2:
player.media.paused = true; // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
utils.dispatchEvent.call(player, player.media, 'pause'); player.embed.unMute();
}
assurePlaybackState.call(player, false);
break; break;
+115 -221
View File
@@ -1,26 +1,24 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.2.2 // plyr.js v3.3.12
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
import { providers, types } from './types'; import captions from './captions';
import defaults from './defaults';
import support from './support';
import utils from './utils';
import Console from './console'; import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen'; import Fullscreen from './fullscreen';
import Listeners from './listeners'; import Listeners from './listeners';
import Storage from './storage';
import Ads from './plugins/ads';
import captions from './captions';
import controls from './controls';
import media from './media'; import media from './media';
import Ads from './plugins/ads';
import source from './source'; import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils';
// Private properties // Private properties
// TODO: Use a WeakMap for private globals // TODO: Use a WeakMap for private globals
@@ -57,6 +55,7 @@ class Plyr {
this.config = utils.extend( this.config = utils.extend(
{}, {},
defaults, defaults,
Plyr.defaults,
options || {}, options || {},
(() => { (() => {
try { try {
@@ -85,7 +84,8 @@ class Plyr {
// Captions // Captions
this.captions = { this.captions = {
active: null, active: null,
currentTrack: null, currentTrack: -1,
meta: new WeakMap(),
}; };
// Fullscreen // Fullscreen
@@ -97,7 +97,6 @@ class Plyr {
this.options = { this.options = {
speed: [], speed: [],
quality: [], quality: [],
captions: [],
}; };
// Debugging // Debugging
@@ -134,17 +133,9 @@ class Plyr {
} }
// Cache original element state for .destroy() // Cache original element state for .destroy()
// TODO: Investigate a better solution as I suspect this causes reported double load issues? const clone = this.media.cloneNode(true);
setTimeout(() => { clone.autoplay = false;
const clone = this.media.cloneNode(true); this.elements.original = clone;
// Prevent the clone autoplaying
if (clone.getAttribute('autoplay')) {
clone.pause();
}
this.elements.original = clone;
}, 0);
// Set media type based on tag or data attribute // Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube // Supported: video, audio, vimeo, youtube
@@ -343,11 +334,6 @@ class Plyr {
return null; return null;
} }
// If ads are enabled, wait for them first
/* if (this.ads.enabled && !this.ads.initialized) {
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
} */
// Return the promise (for HTML5) // Return the promise (for HTML5)
return this.media.play(); return this.media.play();
} }
@@ -363,6 +349,13 @@ class Plyr {
this.media.pause(); this.media.pause();
} }
/**
* Get playing state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended);
}
/** /**
* Get paused state * Get paused state
*/ */
@@ -371,10 +364,10 @@ class Plyr {
} }
/** /**
* Get playing state * Get stopped state
*/ */
get playing() { get stopped() {
return Boolean(this.ready && !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true)); return Boolean(this.paused && this.currentTime === 0);
} }
/** /**
@@ -404,7 +397,8 @@ class Plyr {
*/ */
stop() { stop() {
if (this.isHTML5) { if (this.isHTML5) {
this.media.load(); this.pause();
this.restart();
} else if (utils.is.function(this.media.stop)) { } else if (utils.is.function(this.media.stop)) {
this.media.stop(); this.media.stop();
} }
@@ -438,21 +432,16 @@ class Plyr {
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start) * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
*/ */
set currentTime(input) { set currentTime(input) {
let targetTime = 0; // Bail if media duration isn't available yet
if (!this.duration) {
if (utils.is.number(input)) { return;
targetTime = input;
} }
// Normalise targetTime // Validate input
if (targetTime < 0) { const inputIsValid = utils.is.number(input) && input > 0;
targetTime = 0;
} else if (targetTime > this.duration) {
targetTime = this.duration;
}
// Set // Set
this.media.currentTime = targetTime; this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
// Logging // Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`); this.debug.log(`Seeking to ${this.currentTime} seconds`);
@@ -500,11 +489,11 @@ class Plyr {
// Faux duration set via config // Faux duration set via config
const fauxDuration = parseFloat(this.config.duration); const fauxDuration = parseFloat(this.config.duration);
// True duration // Media duration can be NaN before the media has loaded
const realDuration = this.media ? Number(this.media.duration) : 0; const duration = (this.media || {}).duration || 0;
// If custom duration is funky, use regular duration // If config duration is funky, use regular duration
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration; return fauxDuration || duration;
} }
/** /**
@@ -686,7 +675,7 @@ class Plyr {
quality = Number(input); quality = Number(input);
} }
if (!utils.is.number(quality) || quality === 0) { if (!utils.is.number(quality)) {
quality = this.storage.get('quality'); quality = this.storage.get('quality');
} }
@@ -799,25 +788,23 @@ class Plyr {
} }
/** /**
* Set the poster image for a HTML5 video * Set the poster image for a video
* @param {input} - the URL for the new poster image * @param {input} - the URL for the new poster image
*/ */
set poster(input) { set poster(input) {
if (!this.isHTML5 || !this.isVideo) { if (!this.isVideo) {
this.debug.warn('Poster can only be set on HTML5 video'); this.debug.warn('Poster can only be set for video');
return; return;
} }
if (utils.is.string(input)) { ui.setPoster.call(this, input);
this.media.setAttribute('poster', input);
}
} }
/** /**
* Get the current poster image * Get the current poster image
*/ */
get poster() { get poster() {
if (!this.isHTML5 || !this.isVideo) { if (!this.isVideo) {
return null; return null;
} }
@@ -851,82 +838,51 @@ class Plyr {
} }
// If the method is called without parameter, toggle based on current value // If the method is called without parameter, toggle based on current value
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active); const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Nothing to change...
if (this.captions.active === show) {
return;
}
// Set global
this.captions.active = show;
// Toggle state // Toggle state
utils.toggleState(this.elements.buttons.captions, this.captions.active); utils.toggleState(this.elements.buttons.captions, active);
// Add class hook // Add class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active); utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
// Trigger an event // Update state and trigger event
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); if (active !== this.captions.active) {
this.captions.active = active;
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
}
} }
/** /**
* Set the captions language * Set the caption track by index
* @param {number} - Caption index
*/
set currentTrack(input) {
captions.set.call(this, input);
}
/**
* Get the current caption track index (-1 if disabled)
*/
get currentTrack() {
const { active, currentTrack } = this.captions;
return active ? currentTrack : -1;
}
/**
* 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} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/ */
set language(input) { set language(input) {
// Nothing specified captions.setLanguage.call(this, input);
if (!utils.is.string(input)) {
return;
}
// If empty string is passed, assume disable captions
if (utils.is.empty(input)) {
this.toggleCaptions(false);
return;
}
// Normalize
const language = input.toLowerCase();
// Check for support
if (!this.options.captions.includes(language)) {
this.debug.warn(`Unsupported language option: ${language}`);
return;
}
// Ensure captions are enabled
this.toggleCaptions(true);
// Enabled only
if (language === 'enabled') {
return;
}
// If nothing to change, bail
if (this.language === language) {
return;
}
// Update config
this.captions.language = language;
// Clear caption
captions.setText.call(this, null);
// Update captions
captions.setLanguage.call(this);
// Trigger an event
utils.dispatchEvent.call(this, this.media, 'languagechange');
} }
/** /**
* Get the current captions language * Get the current track's language
*/ */
get language() { get language() {
return this.captions.language; return (captions.getCurrentTrack.call(this) || {}).language;
} }
/** /**
@@ -976,119 +932,32 @@ class Plyr {
/** /**
* Toggle the player controls * Toggle the player controls
* @param {boolean} toggle - Whether to show the controls * @param {boolean} [toggle] - Whether to show the controls
*/ */
toggleControls(toggle) { toggleControls(toggle) {
// We need controls of course... // Don't toggle if missing UI support or if it's audio
if (!utils.is.element(this.elements.controls)) { if (this.supported.ui && !this.isAudio) {
return; // Get state before change
} const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
// Don't hide if no UI support or it's audio // Negate the argument if not undefined since adding the class to hides the controls
if (!this.supported.ui || this.isAudio) { const force = typeof toggle === 'undefined' ? undefined : !toggle;
return;
}
let delay = 0; // Apply and get updated state
let show = toggle; const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
let isEnterFullscreen = false;
// Get toggle state if not set // Close menu
if (!utils.is.boolean(toggle)) { if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
if (utils.is.event(toggle)) { controls.toggleMenu.call(this, false);
// Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen';
// Events that show the controls
const showEvents = [
'touchstart',
'touchmove',
'mouseenter',
'mousemove',
'focusin',
];
// Events that delay hiding
const delayEvents = [
'touchmove',
'touchend',
'mousemove',
];
// Whether to show controls
show = showEvents.includes(toggle.type);
// Delay hiding on move events
if (delayEvents.includes(toggle.type)) {
delay = 2000;
}
// Delay a little more for keyboard users
if (!this.touch && toggle.type === 'focusin') {
delay = 3000;
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
}
} else {
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
} }
} // Trigger event on change
if (hiding !== isHidden) {
// Clear timer on every call const eventName = hiding ? 'controlshidden' : 'controlsshown';
clearTimeout(this.timers.controls); utils.dispatchEvent.call(this, this.media, eventName);
// If the mouse is not over the controls, set a timeout to hide them
if (show || this.paused || this.loading) {
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
// Trigger event
if (toggled) {
utils.dispatchEvent.call(this, this.media, 'controlsshown');
}
// Always show controls when paused or if touch
if (this.paused || this.loading) {
return;
}
// Delay for hiding on touch
if (this.touch) {
delay = 3000;
} }
return !hiding;
} }
return false;
// If toggle is false or if we're playing (regardless of toggle),
// then set the timer to hide the controls
if (!show || this.playing) {
this.timers.controls = setTimeout(() => {
// We need controls of course...
if (!utils.is.element(this.elements.controls)) {
return;
}
// If the mouse is over the controls (and not entering fullscreen), bail
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
return;
}
// Restore transition behaviour
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
// Trigger event and close menu
if (toggled) {
utils.dispatchEvent.call(this, this.media, 'controlshidden');
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
}
}, delay);
}
} }
/** /**
@@ -1250,6 +1119,31 @@ class Plyr {
static loadSprite(url, id) { static loadSprite(url, id) {
return utils.loadSprite(url, id); return utils.loadSprite(url, id);
} }
/**
* Setup multiple instances
* @param {*} selector
* @param {object} options
*/
static setup(selector, options = {}) {
let targets = null;
if (utils.is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) {
targets = Array.from(selector);
} else if (utils.is.array(selector)) {
targets = selector.filter(utils.is.element);
}
if (utils.is.empty(targets)) {
return null;
}
return targets.map(t => new Plyr(t, options));
}
} }
Plyr.defaults = utils.cloneDeep(defaults);
export default Plyr; export default Plyr;
+2 -3
View File
@@ -1,14 +1,13 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.2.2 // plyr.js v3.3.12
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
import 'babel-polyfill'; import 'babel-polyfill';
import 'custom-event-polyfill'; import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr'; import Plyr from './plyr';
export default Plyr; export default Plyr;
+5 -5
View File
@@ -2,12 +2,12 @@
// Plyr source update // Plyr source update
// ========================================================================== // ==========================================================================
import { providers } from './types';
import utils from './utils';
import html5 from './html5'; import html5 from './html5';
import media from './media'; import media from './media';
import ui from './ui';
import support from './support'; import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
const source = { const source = {
// Add elements to HTML5 media (source, tracks, etc) // Add elements to HTML5 media (source, tracks, etc)
@@ -94,8 +94,8 @@ const source = {
if (this.config.autoplay) { if (this.config.autoplay) {
this.media.setAttribute('autoplay', ''); this.media.setAttribute('autoplay', '');
} }
if ('poster' in input) { if (!utils.is.empty(input.poster)) {
this.media.setAttribute('poster', input.poster); this.poster = input.poster;
} }
if (this.config.loop.active) { if (this.config.loop.active) {
this.media.setAttribute('loop', ''); this.media.setAttribute('loop', '');
+1 -1
View File
@@ -31,7 +31,7 @@ class Storage {
} }
get(key) { get(key) {
if (!Storage.supported) { if (!Storage.supported || !this.enabled) {
return null; return null;
} }
+1
View File
@@ -133,6 +133,7 @@ const support = {
}, },
}); });
window.addEventListener('test', null, options); window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
+89 -172
View File
@@ -2,10 +2,14 @@
// Plyr UI // Plyr UI
// ========================================================================== // ==========================================================================
import utils from './utils';
import captions from './captions'; import captions from './captions';
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
const ui = { const ui = {
addStyleHook() { addStyleHook() {
@@ -48,16 +52,13 @@ const ui = {
this.listeners.controls(); this.listeners.controls();
} }
// If there's no controls, bail
if (!utils.is.element(this.elements.controls)) {
return;
}
// Remove native controls // Remove native controls
ui.toggleNativeControls.call(this); ui.toggleNativeControls.call(this);
// Captions // Setup captions for HTML5
captions.setup.call(this); if (this.isHTML5) {
captions.setup.call(this);
}
// Reset volume // Reset volume
this.volume = null; this.volume = null;
@@ -75,14 +76,26 @@ const ui = {
this.quality = null; this.quality = null;
// Reset volume display // Reset volume display
ui.updateVolume.call(this); controls.updateVolume.call(this);
// Reset time display // Reset time display
ui.timeUpdate.call(this); controls.timeUpdate.call(this);
// Update the UI // Update the UI
ui.checkPlaying.call(this); ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls // Ready for API calls
this.ready = true; this.ready = true;
@@ -93,6 +106,17 @@ const ui = {
// Set the title // Set the title
ui.setTitle.call(this); ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
ui.setPoster.call(this, this.poster);
}
// Manually set the duration if user has overridden it.
// The event listeners for it doesn't get called if preload is disabled (#701)
if (this.config.duration) {
controls.durationUpdate.call(this);
}
}, },
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
@@ -126,22 +150,64 @@ const ui = {
// Default to media type // Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', i18n.get('frameTitle', this.config)); iframe.setAttribute('title', format.replace('{title}', title));
} }
}, },
// Toggle poster
togglePoster(enable) {
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Set the poster image (async)
setPoster(poster) {
// Set property regardless of validity
this.media.setAttribute('poster', poster);
// Bail if element is missing
if (!utils.is.element(this.elements.poster)) {
return Promise.reject();
}
// Load the image, and set poster if successful
const loadPromise = utils.loadImage(poster)
.then(() => {
this.elements.poster.style.backgroundImage = `url('${poster}')`;
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
});
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
loadPromise.catch(() => ui.togglePoster.call(this, false));
// Return the promise so the caller can use it as well
return loadPromise;
},
// Check playing state // Check playing state
checkPlaying() { checkPlaying(event) {
// Class hooks // Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused); utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state // Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing); utils.toggleState(this.elements.buttons.play, this.playing);
// Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') {
return;
}
// Toggle controls // Toggle controls
this.toggleControls(!this.playing); ui.toggleControls.call(this);
}, },
// Check if media is loading // Check if media is loading
@@ -156,171 +222,22 @@ const ui = {
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => { this.timers.loading = setTimeout(() => {
// Toggle container class hook // Update progress bar loading class state
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done // Update controls visibility
this.toggleControls(this.loading); ui.toggleControls.call(this);
}, this.loading ? 250 : 0); }, this.loading ? 250 : 0);
}, },
// Check if media failed to load // Toggle controls based on state and `force` argument
checkFailed() { toggleControls(force) {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState const { controls } = this.elements;
this.failed = this.media.networkState === 3;
if (this.failed) { if (controls && this.config.hideControls) {
utils.toggleClass(this.elements.container, this.config.classNames.loading, false); // Show controls if force, loading, paused, or button interaction, otherwise hide
utils.toggleClass(this.elements.container, this.config.classNames.error, true); this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
} }
// Clear timer
clearTimeout(this.timers.failed);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
}, this.loading ? 250 : 0);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.element(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
return;
}
// eslint-disable-next-line
target.value = value;
// Webkit range fill
controls.updateRangeFill.call(this, target);
},
// Set <progress> value
setProgress(target, input) {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
},
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
let value = 0;
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event
if (event.type === 'timeupdate') {
ui.setRange.call(this, this.elements.inputs.seek, value);
}
break;
// Check buffer status
case 'playing':
case 'progress':
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
break;
default:
break;
}
}
},
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target) || !utils.is.number(time)) {
return;
}
// Always display hours if duration is over an hour
const displayHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, displayHours, inverted);
},
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// Playing progress
ui.updateProgress.call(this, event);
},
// Show the duration on metadataloaded
durationUpdate() {
if (!this.supported.ui) {
return;
}
// If there's a spot to display duration
const hasDuration = utils.is.element(this.elements.display.duration);
// If there's only one time display, display duration there
if (!hasDuration && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (hasDuration) {
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
}, },
}; };
+99 -112
View File
@@ -3,74 +3,72 @@
// ========================================================================== // ==========================================================================
import loadjs from 'loadjs'; import loadjs from 'loadjs';
import Storage from './storage';
import support from './support'; import support from './support';
import { providers } from './types'; import { providers } from './types';
const utils = { const utils = {
// Check variable types // Check variable types
is: { is: {
plyr(input) {
return this.instanceof(input, window.Plyr);
},
object(input) { object(input) {
return this.getConstructor(input) === Object; return utils.getConstructor(input) === Object;
}, },
number(input) { number(input) {
return this.getConstructor(input) === Number && !Number.isNaN(input); return utils.getConstructor(input) === Number && !Number.isNaN(input);
}, },
string(input) { string(input) {
return this.getConstructor(input) === String; return utils.getConstructor(input) === String;
}, },
boolean(input) { boolean(input) {
return this.getConstructor(input) === Boolean; return utils.getConstructor(input) === Boolean;
}, },
function(input) { function(input) {
return this.getConstructor(input) === Function; return utils.getConstructor(input) === Function;
}, },
array(input) { array(input) {
return !this.nullOrUndefined(input) && Array.isArray(input); return !utils.is.nullOrUndefined(input) && Array.isArray(input);
}, },
weakMap(input) { weakMap(input) {
return this.instanceof(input, window.WeakMap); return utils.is.instanceof(input, WeakMap);
}, },
nodeList(input) { nodeList(input) {
return this.instanceof(input, window.NodeList); return utils.is.instanceof(input, NodeList);
}, },
element(input) { element(input) {
return this.instanceof(input, window.Element); return utils.is.instanceof(input, Element);
}, },
textNode(input) { textNode(input) {
return this.getConstructor(input) === Text; return utils.getConstructor(input) === Text;
}, },
event(input) { event(input) {
return this.instanceof(input, window.Event); return utils.is.instanceof(input, Event);
}, },
cue(input) { cue(input) {
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue); return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue);
}, },
track(input) { track(input) {
return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind)); return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind));
}, },
url(input) { url(input) {
return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
}, },
nullOrUndefined(input) { nullOrUndefined(input) {
return input === null || typeof input === 'undefined'; return input === null || typeof input === 'undefined';
}, },
empty(input) { empty(input) {
return ( return (
this.nullOrUndefined(input) || utils.is.nullOrUndefined(input) ||
((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) || ((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) ||
(this.object(input) && !Object.keys(input).length) (utils.is.object(input) && !Object.keys(input).length)
); );
}, },
instanceof(input, constructor) { instanceof(input, constructor) {
return Boolean(input && constructor && input instanceof constructor); return Boolean(input && constructor && input instanceof constructor);
}, },
getConstructor(input) { },
return !this.nullOrUndefined(input) ? input.constructor : null;
}, getConstructor(input) {
return !utils.is.nullOrUndefined(input) ? input.constructor : null;
}, },
// Unfortunately, due to mixed support, UA sniffing is required // Unfortunately, due to mixed support, UA sniffing is required
@@ -123,6 +121,21 @@ const utils = {
}); });
}, },
// Load image avoiding xhr/fetch CORS issues
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
// By default it checks if it is at least 1px, but you can add a second argument to change this.
loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, {onload: handler, onerror: handler, src});
});
},
// Load an external script // Load an external script
loadScript(url) { loadScript(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -139,27 +152,28 @@ const utils = {
return; return;
} }
const prefix = 'cache-'; const prefix = 'cache';
const hasId = utils.is.string(id); const hasId = utils.is.string(id);
let isCached = false; let isCached = false;
const exists = () => document.querySelectorAll(`#${id}`).length; const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
function injectSprite(data) {
// Check again incase of race condition // Check again incase of race condition
if (hasId && exists()) { if (hasId && exists()) {
return; return;
} }
// Inject content
this.innerHTML = data;
// Inject the SVG to the body // Inject the SVG to the body
document.body.insertBefore(this, document.body.childNodes[0]); document.body.insertAdjacentElement('afterbegin', container);
} };
// Only load once if ID set // Only load once if ID set
if (!hasId || !exists()) { if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container // Create container
const container = document.createElement('div'); const container = document.createElement('div');
utils.toggleHidden(container, true); utils.toggleHidden(container, true);
@@ -169,14 +183,13 @@ const utils = {
} }
// Check in cache // Check in cache
if (support.storage) { if (useStorage) {
const cached = window.localStorage.getItem(prefix + id); const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null; isCached = cached !== null;
if (isCached) { if (isCached) {
const data = JSON.parse(cached); const data = JSON.parse(cached);
injectSprite.call(container, data.content); update(container, data.content);
return;
} }
} }
@@ -188,16 +201,16 @@ const utils = {
return; return;
} }
if (support.storage) { if (useStorage) {
window.localStorage.setItem( window.localStorage.setItem(
prefix + id, `${prefix}-${id}`,
JSON.stringify({ JSON.stringify({
content: result, content: result,
}), }),
); );
} }
injectSprite.call(container, result); update(container, result);
}) })
.catch(() => {}); .catch(() => {});
} }
@@ -251,7 +264,7 @@ const utils = {
// Add text node // Add text node
if (utils.is.string(text)) { if (utils.is.string(text)) {
element.textContent = text; element.innerText = text;
} }
// Return built element // Return built element
@@ -269,14 +282,14 @@ const utils = {
parent.appendChild(utils.createElement(type, attributes, text)); parent.appendChild(utils.createElement(type, attributes, text));
}, },
// Remove an element // Remove element(s)
removeElement(element) { removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return; return;
} }
if (utils.is.nodeList(element) || utils.is.array(element)) { if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
Array.from(element).forEach(utils.removeElement);
return; return;
} }
@@ -394,14 +407,16 @@ const utils = {
} }
}, },
// Toggle class on an element // Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass(element, className, toggle) { toggleClass(element, className, force) {
if (utils.is.element(element)) { if (utils.is.element(element)) {
const contains = element.classList.contains(className); let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[toggle ? 'add' : 'remove'](className); element.classList[method](className);
return element.classList.contains(className);
return (toggle && !contains) || (!toggle && contains);
} }
return null; return null;
@@ -435,60 +450,6 @@ const utils = {
return this.elements.container.querySelector(selector); return this.elements.container.querySelector(selector);
}, },
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Get the focused element // Get the focused element
getFocusElement() { getFocusElement() {
let focused = document.activeElement; let focused = document.activeElement;
@@ -602,7 +563,7 @@ const utils = {
const event = new CustomEvent(type, { const event = new CustomEvent(type, {
bubbles, bubbles,
detail: Object.assign({}, detail, { detail: Object.assign({}, detail, {
plyr: utils.is.plyr(this) ? this : null, plyr: this,
}), }),
}); });
@@ -632,6 +593,15 @@ const utils = {
element.setAttribute('aria-pressed', state); element.setAttribute('aria-pressed', state);
}, },
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
},
// Get percentage // Get percentage
getPercentage(current, max) { getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
@@ -656,16 +626,16 @@ const utils = {
formatTime(time = 0, displayHours = false, inverted = false) { formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number // Bail if the value isn't a number
if (!utils.is.number(time)) { if (!utils.is.number(time)) {
return this.formatTime(null, displayHours, inverted); return utils.formatTime(null, displayHours, inverted);
} }
// Format time component to add leading zero // Format time component to add leading zero
const format = value => `0${value}`.slice(-2); const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs // Breakdown to hours, mins, secs
let hours = this.getHours(time); let hours = utils.getHours(time);
const mins = this.getMinutes(time); const mins = utils.getMinutes(time);
const secs = this.getSeconds(time); const secs = utils.getSeconds(time);
// Do we need to display hours? // Do we need to display hours?
if (displayHours || hours > 0) { if (displayHours || hours > 0) {
@@ -752,6 +722,16 @@ const utils = {
return array.filter((item, index) => array.indexOf(item) === index); return array.filter((item, index) => array.indexOf(item) === index);
}, },
// Clone nested objects
cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
},
// Get a nested value in an object
getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
},
// Get the closest value in an array // Get the closest value in an array
closest(array, value) { closest(array, value) {
if (!utils.is.array(array) || !array.length) { if (!utils.is.array(array) || !array.length) {
@@ -769,7 +749,7 @@ const utils = {
} }
// Vimeo // Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) { if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo; return providers.vimeo;
} }
@@ -813,10 +793,10 @@ const utils = {
// Parse URL if needed // Parse URL if needed
if (input.startsWith('http://') || input.startsWith('https://')) { if (input.startsWith('http://') || input.startsWith('https://')) {
({ search } = this.parseUrl(input)); ({ search } = utils.parseUrl(input));
} }
if (this.is.empty(search)) { if (utils.is.empty(search)) {
return null; return null;
} }
@@ -852,6 +832,13 @@ const utils = {
return fragment.firstChild.innerText; return fragment.firstChild.innerText;
}, },
// Like outerHTML, but also works for DocumentFragment
getHTML(element) {
const wrapper = document.createElement('div');
wrapper.appendChild(element);
return wrapper.innerHTML;
},
// Get aspect ratio for dimensions // Get aspect ratio for dimensions
getAspectRatio(width, height) { getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
+1
View File
@@ -29,6 +29,7 @@
button { button {
font: inherit; font: inherit;
line-height: inherit; line-height: inherit;
width: auto;
} }
// Ignore focus // Ignore focus
+1 -1
View File
@@ -21,7 +21,7 @@
transition: transform 0.4s ease-in-out; transition: transform 0.4s ease-in-out;
width: 100%; width: 100%;
span { .plyr__caption {
background: $plyr-captions-bg; background: $plyr-captions-bg;
border-radius: 2px; border-radius: 2px;
box-decoration-break: clone; box-decoration-break: clone;
+2 -2
View File
@@ -32,8 +32,8 @@ $embed-padding: ((100 / 16) * 9);
pointer-events: none; pointer-events: none;
} }
// Vimeo hack // Only used for Vimeo
> div { > .plyr__video-embed__container {
padding-bottom: to-percentage($height); padding-bottom: to-percentage($height);
position: relative; position: relative;
transform: translateY(-$offset); transform: translateY(-$offset);
+23
View File
@@ -0,0 +1,23 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
pointer-events: none;
}
.plyr--stopped.plyr__poster-enabled .plyr__poster {
opacity: 1;
}
+17 -8
View File
@@ -5,8 +5,18 @@
.plyr__progress { .plyr__progress {
display: flex; display: flex;
flex: 1; flex: 1;
left: $plyr-range-thumb-height / 2;
margin-right: $plyr-range-thumb-height;
position: relative; position: relative;
input[type='range'],
&__buffer {
margin-left: -($plyr-range-thumb-height / 2);
margin-right: -($plyr-range-thumb-height / 2);
// Offset the range thumb in order to be able to calculate the relative progress (#954)
width: calc(100% + #{$plyr-range-thumb-height});
}
input[type='range'] { input[type='range'] {
position: relative; position: relative;
z-index: 2; z-index: 2;
@@ -19,18 +29,17 @@
} }
} }
.plyr__progress--buffer { .plyr__progress__buffer {
-webkit-appearance: none; /* stylelint-disable-line */ -webkit-appearance: none; /* stylelint-disable-line */
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 100px; border-radius: 100px;
height: $plyr-range-track-height; height: $plyr-range-track-height;
left: 0; left: 0;
margin: -($plyr-range-track-height / 2) 0 0; margin-top: -($plyr-range-track-height / 2);
padding: 0; padding: 0;
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 100%;
&::-webkit-progress-bar { &::-webkit-progress-bar {
background: transparent; background: transparent;
@@ -58,17 +67,17 @@
} }
} }
.plyr--video .plyr__progress--buffer { .plyr--video .plyr__progress__buffer {
box-shadow: 0 1px 1px rgba(#000, 0.15); box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-progress-buffered-bg; color: $plyr-video-progress-buffered-bg;
} }
.plyr--audio .plyr__progress--buffer { .plyr--audio .plyr__progress__buffer {
color: $plyr-audio-progress-buffered-bg; color: $plyr-audio-progress-buffered-bg;
} }
// Loading state // Loading state
.plyr--loading .plyr__progress--buffer { .plyr--loading .plyr__progress__buffer {
animation: plyr-progress 1s linear infinite; animation: plyr-progress 1s linear infinite;
background-image: linear-gradient( background-image: linear-gradient(
-45deg, -45deg,
@@ -85,10 +94,10 @@
color: transparent; color: transparent;
} }
.plyr--video.plyr--loading .plyr__progress--buffer { .plyr--video.plyr--loading .plyr__progress__buffer {
background-color: $plyr-video-progress-buffered-bg; background-color: $plyr-video-progress-buffered-bg;
} }
.plyr--audio.plyr--loading .plyr__progress--buffer { .plyr--audio.plyr--loading .plyr__progress__buffer {
background-color: $plyr-audio-progress-buffered-bg; background-color: $plyr-audio-progress-buffered-bg;
} }
+1
View File
@@ -19,6 +19,7 @@
transform: translate(-50%, 10px) scale(0.8); transform: translate(-50%, 10px) scale(0.8);
transform-origin: 50% 100%; transform-origin: 50% 100%;
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease; transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
white-space: nowrap;
z-index: 2; z-index: 2;
// The background triangle // The background triangle
+7 -2
View File
@@ -23,7 +23,12 @@
// Hide sound controls on iOS // Hide sound controls on iOS
// It's not supported to change volume using JavaScript: // It's not supported to change volume using JavaScript:
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
.plyr--is-ios .plyr__volume, .plyr--is-ios .plyr__volume {
.plyr--is-ios [data-plyr='mute'] { display: none !important;
}
// Vimeo has no toggle mute method so hide mute button
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
display: none !important; display: none !important;
} }
+2 -2
View File
@@ -31,14 +31,14 @@
@import 'components/controls'; @import 'components/controls';
@import 'components/embed'; @import 'components/embed';
@import 'components/menus'; @import 'components/menus';
@import 'components/progress';
@import 'components/sliders'; @import 'components/sliders';
@import 'components/poster';
@import 'components/times'; @import 'components/times';
@import 'components/tooltips'; @import 'components/tooltips';
@import 'components/video'; @import 'components/video';
@import 'components/progress';
@import 'components/volume'; @import 'components/volume';
@import 'states/error';
@import 'states/fullscreen'; @import 'states/fullscreen';
@import 'plugins/ads'; @import 'plugins/ads';
-25
View File
@@ -1,25 +0,0 @@
// --------------------------------------------------------------
// Error state
// --------------------------------------------------------------
.plyr--has-error {
pointer-events: none;
&::after {
align-items: center;
background: rgba(#000, 90%);
color: #fff;
content: attr(data-plyr-error);
display: flex;
font-size: $plyr-font-size-base;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
text-align: center;
text-shadow: 0 1px 1px rgba(#000, 10%);
top: 0;
width: 100%;
z-index: 10;
}
}
+285 -192
View File
@@ -2,48 +2,12 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/code-frame@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.42.tgz#a9c83233fa7cd06b39dc77adbb908616ff4f1962"
dependencies:
"@babel/highlight" "7.0.0-beta.42"
"@babel/code-frame@7.0.0-beta.44": "@babel/code-frame@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9"
dependencies: dependencies:
"@babel/highlight" "7.0.0-beta.44" "@babel/highlight" "7.0.0-beta.44"
"@babel/core@^7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.0.0-beta.42.tgz#b3a838fddbd19663369a0b4892189fd8d3f82001"
dependencies:
"@babel/code-frame" "7.0.0-beta.42"
"@babel/generator" "7.0.0-beta.42"
"@babel/helpers" "7.0.0-beta.42"
"@babel/template" "7.0.0-beta.42"
"@babel/traverse" "7.0.0-beta.42"
"@babel/types" "7.0.0-beta.42"
babylon "7.0.0-beta.42"
convert-source-map "^1.1.0"
debug "^3.1.0"
json5 "^0.5.0"
lodash "^4.2.0"
micromatch "^2.3.11"
resolve "^1.3.2"
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.42.tgz#777bb50f39c94a7e57f73202d833141f8159af33"
dependencies:
"@babel/types" "7.0.0-beta.42"
jsesc "^2.5.1"
lodash "^4.2.0"
source-map "^0.5.0"
trim-right "^1.0.1"
"@babel/generator@7.0.0-beta.44": "@babel/generator@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42"
@@ -54,14 +18,6 @@
source-map "^0.5.0" source-map "^0.5.0"
trim-right "^1.0.1" trim-right "^1.0.1"
"@babel/helper-function-name@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.42.tgz#b38b8f4f85168d1812c543dd700b5d549b0c4658"
dependencies:
"@babel/helper-get-function-arity" "7.0.0-beta.42"
"@babel/template" "7.0.0-beta.42"
"@babel/types" "7.0.0-beta.42"
"@babel/helper-function-name@7.0.0-beta.44": "@babel/helper-function-name@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd"
@@ -70,46 +26,18 @@
"@babel/template" "7.0.0-beta.44" "@babel/template" "7.0.0-beta.44"
"@babel/types" "7.0.0-beta.44" "@babel/types" "7.0.0-beta.44"
"@babel/helper-get-function-arity@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.42.tgz#ad072e32f912c033053fc80478169aeadc22191e"
dependencies:
"@babel/types" "7.0.0-beta.42"
"@babel/helper-get-function-arity@7.0.0-beta.44": "@babel/helper-get-function-arity@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15"
dependencies: dependencies:
"@babel/types" "7.0.0-beta.44" "@babel/types" "7.0.0-beta.44"
"@babel/helper-split-export-declaration@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.42.tgz#0d0d5254220a9cc4e7e226240306b939dc210ee7"
dependencies:
"@babel/types" "7.0.0-beta.42"
"@babel/helper-split-export-declaration@7.0.0-beta.44": "@babel/helper-split-export-declaration@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc"
dependencies: dependencies:
"@babel/types" "7.0.0-beta.44" "@babel/types" "7.0.0-beta.44"
"@babel/helpers@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.0.0-beta.42.tgz#151c1c4e9da1b6ce83d54c1be5fb8c9c57aa5044"
dependencies:
"@babel/template" "7.0.0-beta.42"
"@babel/traverse" "7.0.0-beta.42"
"@babel/types" "7.0.0-beta.42"
"@babel/highlight@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.42.tgz#a502a1c0d6f99b2b0e81d468a1b0c0e81e3f3623"
dependencies:
chalk "^2.0.0"
esutils "^2.0.2"
js-tokens "^3.0.0"
"@babel/highlight@7.0.0-beta.44": "@babel/highlight@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5"
@@ -118,15 +46,6 @@
esutils "^2.0.2" esutils "^2.0.2"
js-tokens "^3.0.0" js-tokens "^3.0.0"
"@babel/template@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.42.tgz#7186d4e70d44cdec975049ba0a73bdaf5cdee052"
dependencies:
"@babel/code-frame" "7.0.0-beta.42"
"@babel/types" "7.0.0-beta.42"
babylon "7.0.0-beta.42"
lodash "^4.2.0"
"@babel/template@7.0.0-beta.44": "@babel/template@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
@@ -136,21 +55,6 @@
babylon "7.0.0-beta.44" babylon "7.0.0-beta.44"
lodash "^4.2.0" lodash "^4.2.0"
"@babel/traverse@7.0.0-beta.42", "@babel/traverse@^7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.42.tgz#f4bf4d1e33d41baf45205e2d0463591d57326285"
dependencies:
"@babel/code-frame" "7.0.0-beta.42"
"@babel/generator" "7.0.0-beta.42"
"@babel/helper-function-name" "7.0.0-beta.42"
"@babel/helper-split-export-declaration" "7.0.0-beta.42"
"@babel/types" "7.0.0-beta.42"
babylon "7.0.0-beta.42"
debug "^3.1.0"
globals "^11.1.0"
invariant "^2.2.0"
lodash "^4.2.0"
"@babel/traverse@7.0.0-beta.44": "@babel/traverse@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966"
@@ -166,14 +70,6 @@
invariant "^2.2.0" invariant "^2.2.0"
lodash "^4.2.0" lodash "^4.2.0"
"@babel/types@7.0.0-beta.42":
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.42.tgz#1e2118767684880f6963801b272fd2b3348efacc"
dependencies:
esutils "^2.0.2"
lodash "^4.2.0"
to-fast-properties "^2.0.0"
"@babel/types@7.0.0-beta.44": "@babel/types@7.0.0-beta.44":
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757"
@@ -517,6 +413,30 @@ babel-core@^6.26.0:
slash "^1.0.0" slash "^1.0.0"
source-map "^0.5.6" source-map "^0.5.6"
babel-core@^6.26.3:
version "6.26.3"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
dependencies:
babel-code-frame "^6.26.0"
babel-generator "^6.26.0"
babel-helpers "^6.24.1"
babel-messages "^6.23.0"
babel-register "^6.26.0"
babel-runtime "^6.26.0"
babel-template "^6.26.0"
babel-traverse "^6.26.0"
babel-types "^6.26.0"
babylon "^6.18.0"
convert-source-map "^1.5.1"
debug "^2.6.9"
json5 "^0.5.1"
lodash "^4.17.4"
minimatch "^3.0.4"
path-is-absolute "^1.0.1"
private "^0.1.8"
slash "^1.0.0"
source-map "^0.5.7"
babel-eslint@^8.2.3: babel-eslint@^8.2.3:
version "8.2.3" version "8.2.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf"
@@ -877,9 +797,9 @@ babel-polyfill@^6.26.0:
core-js "^2.5.0" core-js "^2.5.0"
regenerator-runtime "^0.10.5" regenerator-runtime "^0.10.5"
babel-preset-env@^1.6.1: babel-preset-env@^1.7.0:
version "1.6.1" version "1.7.0"
resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
dependencies: dependencies:
babel-plugin-check-es2015-constants "^6.22.0" babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-syntax-trailing-function-commas "^6.22.0" babel-plugin-syntax-trailing-function-commas "^6.22.0"
@@ -908,7 +828,7 @@ babel-preset-env@^1.6.1:
babel-plugin-transform-es2015-unicode-regex "^6.22.0" babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-exponentiation-operator "^6.22.0" babel-plugin-transform-exponentiation-operator "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0" babel-plugin-transform-regenerator "^6.22.0"
browserslist "^2.1.2" browserslist "^3.2.6"
invariant "^2.2.2" invariant "^2.2.2"
semver "^5.3.0" semver "^5.3.0"
@@ -964,10 +884,6 @@ babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
lodash "^4.17.4" lodash "^4.17.4"
to-fast-properties "^1.0.3" to-fast-properties "^1.0.3"
babylon@7.0.0-beta.42, babylon@^7.0.0-beta.42:
version "7.0.0-beta.42"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.42.tgz#67cfabcd4f3ec82999d29031ccdea89d0ba99657"
babylon@7.0.0-beta.44: babylon@7.0.0-beta.44:
version "7.0.0-beta.44" version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
@@ -1006,9 +922,9 @@ beeper@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
binaryextensions@~1.0.0: binaryextensions@2:
version "1.0.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-1.0.1.tgz#1e637488b35b58bda5f4774bf96a5212a8c90755" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
block-stream@*: block-stream@*:
version "0.0.9" version "0.0.9"
@@ -1082,7 +998,7 @@ braces@^2.3.1:
split-string "^3.0.2" split-string "^3.0.2"
to-regex "^3.0.1" to-regex "^3.0.1"
browserslist@^2.1.2, browserslist@^2.11.3: browserslist@^2.11.3:
version "2.11.3" version "2.11.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2"
dependencies: dependencies:
@@ -1096,6 +1012,13 @@ browserslist@^3.1.1:
caniuse-lite "^1.0.30000813" caniuse-lite "^1.0.30000813"
electron-to-chromium "^1.3.36" electron-to-chromium "^1.3.36"
browserslist@^3.2.6:
version "3.2.8"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
dependencies:
caniuse-lite "^1.0.30000844"
electron-to-chromium "^1.3.47"
builtin-modules@^1.0.0: builtin-modules@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -1163,6 +1086,10 @@ caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.300008
version "1.0.30000815" version "1.0.30000815"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000815.tgz#3a4258e6850362185adb11b0d754a48402d35bf6" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000815.tgz#3a4258e6850362185adb11b0d754a48402d35bf6"
caniuse-lite@^1.0.30000844:
version "1.0.30000847"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000847.tgz#be77f439be29bbc57ae08004b1e470b653b1ec1d"
capture-stack-trace@^1.0.0: capture-stack-trace@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
@@ -1207,6 +1134,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3
escape-string-regexp "^1.0.5" escape-string-regexp "^1.0.5"
supports-color "^5.3.0" supports-color "^5.3.0"
chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
character-entities-html4@^1.0.0: character-entities-html4@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.1.tgz#359a2a4a0f7e29d3dc2ac99bdbe21ee39438ea50" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.1.tgz#359a2a4a0f7e29d3dc2ac99bdbe21ee39438ea50"
@@ -1391,9 +1326,9 @@ commander@^2.9.0:
version "2.15.0" version "2.15.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322"
commander@~2.13.0: commander@~2.14.1:
version "2.13.0" version "2.14.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
common-tags@^1.4.0: common-tags@^1.4.0:
version "1.7.2" version "1.7.2"
@@ -1417,6 +1352,12 @@ concat-stream@^1.6.0:
readable-stream "^2.2.2" readable-stream "^2.2.2"
typedarray "^0.0.6" typedarray "^0.0.6"
concat-with-sourcemaps@*:
version "1.1.0"
resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e"
dependencies:
source-map "^0.6.1"
concat-with-sourcemaps@^1.0.0: concat-with-sourcemaps@^1.0.0:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.5.tgz#8964bc2347d05819b63798104d87d6e001bed8d0" resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.5.tgz#8964bc2347d05819b63798104d87d6e001bed8d0"
@@ -1442,7 +1383,7 @@ contains-path@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.5.0: convert-source-map@1.X, convert-source-map@^1.5.0, convert-source-map@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
@@ -1458,6 +1399,18 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
dependencies:
is-directory "^0.3.1"
js-yaml "^3.4.3"
minimist "^1.2.0"
object-assign "^4.1.0"
os-homedir "^1.0.1"
parse-json "^2.2.0"
require-from-string "^1.1.0"
cosmiconfig@^3.0.1, cosmiconfig@^3.1.0: cosmiconfig@^3.0.1, cosmiconfig@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397"
@@ -1476,6 +1429,14 @@ cosmiconfig@^4.0.0:
parse-json "^4.0.0" parse-json "^4.0.0"
require-from-string "^2.0.1" require-from-string "^2.0.1"
cosmiconfig@^5.0.0:
version "5.0.5"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.5.tgz#a809e3c2306891ce17ab70359dc8bdf661fe2cd0"
dependencies:
is-directory "^0.3.1"
js-yaml "^3.9.0"
parse-json "^4.0.0"
create-error-class@^3.0.0: create-error-class@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
@@ -1535,6 +1496,10 @@ css@2.X, css@^2.2.1:
source-map-resolve "^0.3.0" source-map-resolve "^0.3.0"
urix "^0.1.0" urix "^0.1.0"
cssesc@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-1.0.1.tgz#ef7bd8d0229ed6a3a7051ff7771265fe7330e0a8"
csso@~2.3.1: csso@~2.3.1:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
@@ -1783,10 +1748,18 @@ ecc-jsbn@~0.1.1:
dependencies: dependencies:
jsbn "~0.1.0" jsbn "~0.1.0"
editions@^1.3.3:
version "1.3.4"
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.36: electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.36:
version "1.3.38" version "1.3.38"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.38.tgz#49234b00c0592f62921f9426bccefee23de086bb" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.38.tgz#49234b00c0592f62921f9426bccefee23de086bb"
electron-to-chromium@^1.3.47:
version "1.3.48"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900"
end-of-stream@~0.1.5: end-of-stream@~0.1.5:
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf"
@@ -1864,9 +1837,9 @@ eslint-module-utils@^2.2.0:
debug "^2.6.8" debug "^2.6.8"
pkg-dir "^1.0.0" pkg-dir "^1.0.0"
eslint-plugin-import@^2.11.0: eslint-plugin-import@^2.12.0:
version "2.11.0" version "2.12.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.11.0.tgz#15aeea37a67499d848e8e981806d4627b5503816" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz#dad31781292d6664b25317fd049d2e2b2f02205d"
dependencies: dependencies:
contains-path "^0.1.0" contains-path "^0.1.0"
debug "^2.6.8" debug "^2.6.8"
@@ -2653,9 +2626,9 @@ gulp-better-rollup@^3.1.0:
vinyl "^2.1.0" vinyl "^2.1.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
gulp-clean-css@^3.9.3: gulp-clean-css@^3.9.4:
version "3.9.3" version "3.9.4"
resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-3.9.3.tgz#47bf7ad62f44970f86e4ac4bdeed68ad904e65c5" resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-3.9.4.tgz#c6d3f8bb7a600fbe661962a72348a330954d343b"
dependencies: dependencies:
clean-css "4.1.11" clean-css "4.1.11"
plugin-error "1.0.1" plugin-error "1.0.1"
@@ -2678,6 +2651,14 @@ gulp-filter@^5.1.0:
plugin-error "^0.1.2" plugin-error "^0.1.2"
streamfilter "^1.0.5" streamfilter "^1.0.5"
gulp-header@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-2.0.5.tgz#16e229c73593ade301168024fea68dab75d9d38c"
dependencies:
concat-with-sourcemaps "*"
lodash.template "^4.4.0"
through2 "^2.0.0"
gulp-open@^3.0.1: gulp-open@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/gulp-open/-/gulp-open-3.0.1.tgz#a2f747b4aa31abec9399b527158b0368c57e2102" resolved "https://registry.yarnpkg.com/gulp-open/-/gulp-open-3.0.1.tgz#a2f747b4aa31abec9399b527158b0368c57e2102"
@@ -2687,15 +2668,25 @@ gulp-open@^3.0.1:
plugin-log "^0.1.0" plugin-log "^0.1.0"
through2 "^2.0.1" through2 "^2.0.1"
gulp-rename@^1.2.2: gulp-postcss@^7.0.1:
version "1.2.2" version "7.0.1"
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.2.2.tgz#3ad4428763f05e2764dec1c67d868db275687817" resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-7.0.1.tgz#3f1c36db1197140c399c252ddff339129638e395"
gulp-replace@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/gulp-replace/-/gulp-replace-0.6.1.tgz#11bf8c8fce533e33e2f6a8f2f430b955ba0be066"
dependencies: dependencies:
istextorbinary "1.0.2" fancy-log "^1.3.2"
plugin-error "^0.1.2"
postcss "^6.0.0"
postcss-load-config "^1.2.0"
vinyl-sourcemaps-apply "^0.2.1"
gulp-rename@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.2.3.tgz#37b75298e9d3e6c0fe9ac4eac13ce3be5434646b"
gulp-replace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gulp-replace/-/gulp-replace-1.0.0.tgz#b32bd61654d97b8d78430a67b3e8ce067b7c9143"
dependencies:
istextorbinary "2.2.1"
readable-stream "^2.0.1" readable-stream "^2.0.1"
replacestream "^4.0.0" replacestream "^4.0.0"
@@ -2766,13 +2757,13 @@ gulp-svgstore@^6.1.1:
plugin-error "^0.1.2" plugin-error "^0.1.2"
vinyl "^2.1.0" vinyl "^2.1.0"
gulp-uglify-es@^1.0.1: gulp-uglify-es@^1.0.4:
version "1.0.1" version "1.0.4"
resolved "https://registry.yarnpkg.com/gulp-uglify-es/-/gulp-uglify-es-1.0.1.tgz#9f991de31c646fb37fe589086ffd3f6e2f9e20f1" resolved "https://registry.yarnpkg.com/gulp-uglify-es/-/gulp-uglify-es-1.0.4.tgz#59ee0d5ea98c1e09c6eaa58c8b018a6ad33f48d4"
dependencies: dependencies:
o-stream "^0.2.2" o-stream "^0.2.2"
plugin-error "^1.0.1" plugin-error "^1.0.1"
uglify-es "^3.3.9" terser "^3.7.5"
vinyl "^2.1.0" vinyl "^2.1.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
@@ -3430,12 +3421,13 @@ isstream@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
istextorbinary@1.0.2: istextorbinary@2.2.1:
version "1.0.2" version "2.2.1"
resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-1.0.2.tgz#ace19354d1a9a0173efeb1084ce0f87b0ad7decf" resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
dependencies: dependencies:
binaryextensions "~1.0.0" binaryextensions "2"
textextensions "~1.0.0" editions "^1.3.3"
textextensions "2"
js-base64@^2.1.8, js-base64@^2.1.9: js-base64@^2.1.8, js-base64@^2.1.9:
version "2.4.3" version "2.4.3"
@@ -3445,7 +3437,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
js-yaml@^3.9.0, js-yaml@^3.9.1: js-yaml@^3.4.3, js-yaml@^3.9.0, js-yaml@^3.9.1:
version "3.11.0" version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
dependencies: dependencies:
@@ -3495,7 +3487,7 @@ json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
json5@^0.5.0, json5@^0.5.1: json5@^0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
@@ -3679,7 +3671,7 @@ lodash._reinterpolate@^2.4.1, lodash._reinterpolate@~2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz#4f1227aa5a8711fc632f5b07a1f4607aab8b3222" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz#4f1227aa5a8711fc632f5b07a1f4607aab8b3222"
lodash._reinterpolate@^3.0.0: lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -3845,6 +3837,13 @@ lodash.template@^3.0.0:
lodash.restparam "^3.0.0" lodash.restparam "^3.0.0"
lodash.templatesettings "^3.0.0" lodash.templatesettings "^3.0.0"
lodash.template@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
dependencies:
lodash._reinterpolate "~3.0.0"
lodash.templatesettings "^4.0.0"
lodash.templatesettings@^3.0.0: lodash.templatesettings@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
@@ -3852,6 +3851,12 @@ lodash.templatesettings@^3.0.0:
lodash._reinterpolate "^3.0.0" lodash._reinterpolate "^3.0.0"
lodash.escape "^3.0.0" lodash.escape "^3.0.0"
lodash.templatesettings@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
dependencies:
lodash._reinterpolate "~3.0.0"
lodash.templatesettings@~2.4.1: lodash.templatesettings@~2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz#ea76c75d11eb86d4dbe89a83893bb861929ac699" resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz#ea76c75d11eb86d4dbe89a83893bb861929ac699"
@@ -4033,6 +4038,20 @@ meow@^4.0.0:
redent "^2.0.0" redent "^2.0.0"
trim-newlines "^2.0.0" trim-newlines "^2.0.0"
meow@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4"
dependencies:
camelcase-keys "^4.0.0"
decamelize-keys "^1.0.0"
loud-rejection "^1.0.0"
minimist-options "^3.0.1"
normalize-package-data "^2.3.4"
read-pkg-up "^3.0.0"
redent "^2.0.0"
trim-newlines "^2.0.0"
yargs-parser "^10.0.0"
merge2@^1.2.1: merge2@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.1.tgz#271d2516ff52d4af7f7b710b8bf3e16e183fef66" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.1.tgz#271d2516ff52d4af7f7b710b8bf3e16e183fef66"
@@ -4213,8 +4232,8 @@ nanomatch@^1.2.9:
to-regex "^3.0.1" to-regex "^3.0.1"
natives@^1.1.0: natives@^1.1.0:
version "1.1.1" version "1.1.3"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.1.tgz#011acce1f7cbd87f7ba6b3093d6cd9392be1c574" resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.3.tgz#44a579be64507ea2d6ed1ca04a9415915cf75558"
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
@@ -4430,7 +4449,7 @@ ordered-read-streams@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126"
os-homedir@^1.0.0: os-homedir@^1.0.0, os-homedir@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@@ -4681,6 +4700,13 @@ postcss-bem-linter@^3.0.0:
postcss "^6.0.6" postcss "^6.0.6"
postcss-resolve-nested-selector "^0.1.1" postcss-resolve-nested-selector "^0.1.1"
postcss-custom-properties@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-7.0.0.tgz#24dc4fbe6d6ed550ea4fd3b11204660e9ffa3b33"
dependencies:
balanced-match "^1.0.0"
postcss "^6.0.18"
postcss-html@^0.12.0: postcss-html@^0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.12.0.tgz#39b6adb4005dfc5464df7999c0f81c95bced7e50" resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.12.0.tgz#39b6adb4005dfc5464df7999c0f81c95bced7e50"
@@ -4697,16 +4723,11 @@ postcss-html@^0.15.0:
remark "^9.0.0" remark "^9.0.0"
unist-util-find-all-after "^1.0.1" unist-util-find-all-after "^1.0.1"
postcss-html@^0.18.0: postcss-html@^0.23.6:
version "0.18.0" version "0.23.7"
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.18.0.tgz#992a84117cc56f9f28915fbadba576489641e652" resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.23.7.tgz#47146c15e21b9c00746c40115dcff8270c439f32"
dependencies: dependencies:
"@babel/core" "^7.0.0-beta.42"
"@babel/traverse" "^7.0.0-beta.42"
babylon "^7.0.0-beta.42"
htmlparser2 "^3.9.2" htmlparser2 "^3.9.2"
remark "^9.0.0"
unist-util-find-all-after "^1.0.1"
postcss-less@^1.1.0: postcss-less@^1.1.0:
version "1.1.3" version "1.1.3"
@@ -4720,6 +4741,36 @@ postcss-less@^1.1.5:
dependencies: dependencies:
postcss "^5.2.16" postcss "^5.2.16"
postcss-load-config@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a"
dependencies:
cosmiconfig "^2.1.0"
object-assign "^4.1.0"
postcss-load-options "^1.2.0"
postcss-load-plugins "^2.3.0"
postcss-load-options@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c"
dependencies:
cosmiconfig "^2.1.0"
object-assign "^4.1.0"
postcss-load-plugins@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92"
dependencies:
cosmiconfig "^2.1.1"
object-assign "^4.1.0"
postcss-markdown@^0.23.6:
version "0.23.7"
resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.23.7.tgz#7e3a398794295c425e51e4f0abdee6d13ad3d134"
dependencies:
remark "^9.0.0"
unist-util-find-all-after "^1.0.2"
postcss-media-query-parser@^0.2.3: postcss-media-query-parser@^0.2.3:
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
@@ -4771,6 +4822,14 @@ postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1:
indexes-of "^1.0.1" indexes-of "^1.0.1"
uniq "^1.0.1" uniq "^1.0.1"
postcss-selector-parser@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-4.0.0.tgz#50c6570f40579036d8e63f23e6c0626fe5743527"
dependencies:
cssesc "^1.0.1"
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-sorting@^3.1.0: postcss-sorting@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-3.1.0.tgz#af7c90ee73ad12569a57664eaf06735c2e25bec0" resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-3.1.0.tgz#af7c90ee73ad12569a57664eaf06735c2e25bec0"
@@ -4778,6 +4837,10 @@ postcss-sorting@^3.1.0:
lodash "^4.17.4" lodash "^4.17.4"
postcss "^6.0.13" postcss "^6.0.13"
postcss-syntax@^0.9.0:
version "0.9.1"
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.9.1.tgz#5dbd90af1631ab8805b8f594bef2c2e8002d3758"
postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
@@ -4799,6 +4862,14 @@ postcss@^5.2.16:
source-map "^0.5.6" source-map "^0.5.6"
supports-color "^3.2.3" supports-color "^3.2.3"
postcss@^6.0.0, postcss@^6.0.18:
version "6.0.22"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
dependencies:
chalk "^2.4.1"
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^6.0.17: postcss@^6.0.17:
version "6.0.20" version "6.0.20"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.20.tgz#686107e743a12d5530cb68438c590d5b2bf72c3c" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.20.tgz#686107e743a12d5530cb68438c590d5b2bf72c3c"
@@ -4874,7 +4945,7 @@ pretty-hrtime@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
private@^0.1.6, private@^0.1.7: private@^0.1.6, private@^0.1.7, private@^0.1.8:
version "0.1.8" version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -4917,9 +4988,9 @@ randomatic@^1.1.3:
is-number "^3.0.0" is-number "^3.0.0"
kind-of "^4.0.0" kind-of "^4.0.0"
raven-js@^3.24.0: raven-js@^3.26.1:
version "3.24.0" version "3.26.1"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.0.tgz#59464d8bc4b3812ae87a282e9bb98ecad5b4b047" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.1.tgz#13f78804f2bed524a7283382e1bca7ab423950a3"
rc@^1.0.1, rc@^1.1.6: rc@^1.0.1, rc@^1.1.6:
version "1.2.6" version "1.2.6"
@@ -5277,6 +5348,10 @@ require-directory@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
require-from-string@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
require-from-string@^2.0.1: require-from-string@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff"
@@ -5331,12 +5406,6 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.5.0:
dependencies: dependencies:
path-parse "^1.0.5" path-parse "^1.0.5"
resolve@^1.3.2:
version "1.6.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c"
dependencies:
path-parse "^1.0.5"
resolve@^1.6.0: resolve@^1.6.0:
version "1.7.1" version "1.7.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
@@ -5366,9 +5435,9 @@ rollup-plugin-babel@^3.0.4:
dependencies: dependencies:
rollup-pluginutils "^1.5.0" rollup-pluginutils "^1.5.0"
rollup-plugin-commonjs@^9.1.0: rollup-plugin-commonjs@^9.1.3:
version "9.1.0" version "9.1.3"
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.0.tgz#468341aab32499123ee9a04b22f51d9bf26fdd94" resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.3.tgz#37bfbf341292ea14f512438a56df8f9ca3ba4d67"
dependencies: dependencies:
estree-walker "^0.5.1" estree-walker "^0.5.1"
magic-string "^0.22.4" magic-string "^0.22.4"
@@ -5461,7 +5530,7 @@ semver-diff@^2.0.0:
dependencies: dependencies:
semver "^5.0.3" semver "^5.0.3"
"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1: "semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0:
version "5.5.0" version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@@ -5854,14 +5923,14 @@ stylelint-scss@^2.0.0:
postcss-selector-parser "^3.1.1" postcss-selector-parser "^3.1.1"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
stylelint-scss@^3.0.1: stylelint-scss@^3.1.0:
version "3.0.1" version "3.1.0"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.0.1.tgz#bc062e818add985f19dee98f7f5b4bff4f38706e" resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.0.tgz#aa46503014d1a6edb2fb4c5fefb73a7d0d5bc644"
dependencies: dependencies:
lodash "^4.17.4" lodash "^4.17.4"
postcss-media-query-parser "^0.2.3" postcss-media-query-parser "^0.2.3"
postcss-resolve-nested-selector "^0.1.1" postcss-resolve-nested-selector "^0.1.1"
postcss-selector-parser "^3.1.1" postcss-selector-parser "^4.0.0"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
stylelint-selector-bem-pattern@^2.0.0: stylelint-selector-bem-pattern@^2.0.0:
@@ -5962,14 +6031,14 @@ stylelint@^8.1.1:
svg-tags "^1.0.0" svg-tags "^1.0.0"
table "^4.0.1" table "^4.0.1"
stylelint@^9.2.0: stylelint@^9.2.1:
version "9.2.0" version "9.2.1"
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.2.0.tgz#f77a82518106074c1a795e962fd780da2c8af43b" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.2.1.tgz#fe63c169f6cd3bc81e77f0e3c6443df3267ec211"
dependencies: dependencies:
autoprefixer "^8.0.0" autoprefixer "^8.0.0"
balanced-match "^1.0.0" balanced-match "^1.0.0"
chalk "^2.0.1" chalk "^2.4.1"
cosmiconfig "^4.0.0" cosmiconfig "^5.0.0"
debug "^3.0.0" debug "^3.0.0"
execall "^1.0.0" execall "^1.0.0"
file-entry-cache "^2.0.0" file-entry-cache "^2.0.0"
@@ -5984,13 +6053,14 @@ stylelint@^9.2.0:
lodash "^4.17.4" lodash "^4.17.4"
log-symbols "^2.0.0" log-symbols "^2.0.0"
mathml-tag-names "^2.0.1" mathml-tag-names "^2.0.1"
meow "^4.0.0" meow "^5.0.0"
micromatch "^2.3.11" micromatch "^2.3.11"
normalize-selector "^0.2.0" normalize-selector "^0.2.0"
pify "^3.0.0" pify "^3.0.0"
postcss "^6.0.16" postcss "^6.0.16"
postcss-html "^0.18.0" postcss-html "^0.23.6"
postcss-less "^1.1.5" postcss-less "^1.1.5"
postcss-markdown "^0.23.6"
postcss-media-query-parser "^0.2.3" postcss-media-query-parser "^0.2.3"
postcss-reporter "^5.0.0" postcss-reporter "^5.0.0"
postcss-resolve-nested-selector "^0.1.1" postcss-resolve-nested-selector "^0.1.1"
@@ -5998,6 +6068,7 @@ stylelint@^9.2.0:
postcss-sass "^0.3.0" postcss-sass "^0.3.0"
postcss-scss "^1.0.2" postcss-scss "^1.0.2"
postcss-selector-parser "^3.1.0" postcss-selector-parser "^3.1.0"
postcss-syntax "^0.9.0"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
resolve-from "^4.0.0" resolve-from "^4.0.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
@@ -6034,6 +6105,12 @@ supports-color@^5.2.0, supports-color@^5.3.0:
dependencies: dependencies:
has-flag "^3.0.0" has-flag "^3.0.0"
supports-color@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
dependencies:
has-flag "^3.0.0"
svg-tags@^1.0.0: svg-tags@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
@@ -6108,13 +6185,20 @@ term-size@^1.2.0:
dependencies: dependencies:
execa "^0.7.0" execa "^0.7.0"
terser@^3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/terser/-/terser-3.7.5.tgz#b18090210794c79a5774bc1f0ebe80fb877a31bd"
dependencies:
commander "~2.14.1"
source-map "~0.6.1"
text-table@~0.2.0: text-table@~0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
textextensions@~1.0.0: textextensions@2:
version "1.0.2" version "2.2.0"
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-1.0.2.tgz#65486393ee1f2bb039a60cbba05b0b68bd9501d2" resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
through2@2.0.3, through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.3: through2@2.0.3, through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.3:
version "2.0.3" version "2.0.3"
@@ -6269,13 +6353,6 @@ typescript@^2.5.1:
version "2.7.2" version "2.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836"
uglify-es@^3.3.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
dependencies:
commander "~2.13.0"
source-map "~0.6.1"
unc-path-regex@^0.1.2: unc-path-regex@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@@ -6328,6 +6405,12 @@ unist-util-find-all-after@^1.0.1:
dependencies: dependencies:
unist-util-is "^2.0.0" unist-util-is "^2.0.0"
unist-util-find-all-after@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.2.tgz#9be49cfbae5ca1566b27536670a92836bf2f8d6d"
dependencies:
unist-util-is "^2.0.0"
unist-util-is@^2.0.0, unist-util-is@^2.1.1: unist-util-is@^2.0.0, unist-util-is@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b"
@@ -6389,6 +6472,10 @@ url-parse-lax@^1.0.0:
dependencies: dependencies:
prepend-http "^1.0.1" prepend-http "^1.0.1"
url-polyfill@^1.0.13:
version "1.0.13"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.0.13.tgz#5324c5e6e3d3bafdc93a73c21def21e8b697c910"
use@^3.1.0: use@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
@@ -6596,6 +6683,12 @@ yallist@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
yargs-parser@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.0.0.tgz#c737c93de2567657750cb1f2c00be639fd19c994"
dependencies:
camelcase "^4.1.0"
yargs-parser@^5.0.0: yargs-parser@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"