Merge branch 'develop' into a11y-improvements

# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/controls.js
#	src/js/fullscreen.js
#	src/js/plyr.js
#	src/js/ui.js
#	src/js/utils.js
This commit is contained in:
Sam Potts
2018-06-17 01:26:24 +10:00
51 changed files with 27362 additions and 27557 deletions
+1 -3
View File
@@ -32,9 +32,7 @@
"message": "Use local parameter instead." "message": "Use local parameter instead."
} }
], ],
"no-param-reassign": [2, { "props": false }], "no-param-reassign": [2, { "props": false }]
"array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }]
}, },
"parserOptions": { "parserOptions": {
"sourceType": "module" "sourceType": "module"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 4,
"printWidth": 160, "printWidth": 120,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all"
} }
+1 -1
View File
@@ -21,7 +21,7 @@
Again, more changes from @friday! Again, more changes from @friday!
- Restore window reference in `utils.is.cue()` - Restore window reference in `is.cue()`
- Fix InvalidStateError and IE11 issues - Fix InvalidStateError and IE11 issues
- Respect storage being disabled for storage getter - Respect storage being disabled for storage getter
+4384 -4367
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
+7679 -7772
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
+13310 -13406
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
+5 -5
View File
@@ -20,14 +20,14 @@
"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.2.1",
"gulp-clean-css": "^3.9.4", "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-header": "^2.0.5",
"gulp-open": "^3.0.1", "gulp-open": "^3.0.1",
"gulp-postcss": "^7.0.1", "gulp-postcss": "^7.0.1",
"gulp-rename": "^1.2.3", "gulp-rename": "^1.3.0",
"gulp-replace": "^1.0.0", "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",
@@ -44,12 +44,12 @@
"rollup-plugin-commonjs": "^9.1.3", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",
"stylelint": "^9.2.1", "stylelint": "^9.3.0",
"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.1.0", "stylelint-scss": "^3.1.2",
"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"],
@@ -74,7 +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.26.1", "raven-js": "^3.26.2",
"url-polyfill": "^1.0.13" "url-polyfill": "^1.0.13"
} }
} }
+2 -1
View File
@@ -11,7 +11,8 @@
}, },
// Exclude from search // Exclude from search
"search.exclude": { "search.exclude": {
"dist/": true "dist/": true,
"demo/dist/": true
}, },
// Linting // Linting
"stylelint.enable": true, "stylelint.enable": true,
+2 -1
View File
@@ -215,7 +215,7 @@ You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) * A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) * A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* 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. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below. _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.
@@ -367,6 +367,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `airplay()` | - | Trigger the airplay dialog on supported devices. | | `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. | | `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. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `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. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. | | `destroy()` | - | Destroy the instance and garbage collect any elements. |
+179 -85
View File
@@ -6,7 +6,21 @@
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support'; import support from './support';
import utils from './utils'; import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
import is from './utils/is';
import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = { const captions = {
// Setup captions // Setup captions
@@ -19,7 +33,11 @@ const captions = {
// 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
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this); controls.setCaptionsMenu.call(this);
} }
@@ -27,15 +45,12 @@ const captions = {
} }
// Inject the container // Inject the container
if (!utils.is.element(this.elements.captions)) { if (!is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
utils.insertAfter(this.elements.captions, this.elements.wrapper); insertAfter(this.elements.captions, this.elements.wrapper);
} }
// Get browser info
const browser = utils.getBrowser();
// Fix IE captions if CORS is used // Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!) // Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) { if (browser.isIE && window.URL) {
@@ -43,84 +58,96 @@ const captions = {
Array.from(elements).forEach(track => { Array.from(elements).forEach(track => {
const src = track.getAttribute('src'); const src = track.getAttribute('src');
const href = utils.parseUrl(src); const url = parseUrl(src);
if (href.hostname !== window.location.href.hostname && [ if (
'http:', url !== null &&
'https:', url.hostname !== window.location.href.hostname &&
].includes(href.protocol)) { ['http:', 'https:'].includes(url.protocol)
utils ) {
.fetch(src, 'blob') fetch(src, 'blob')
.then(blob => { .then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob)); track.setAttribute('src', window.URL.createObjectURL(blob));
}) })
.catch(() => { .catch(() => {
utils.removeElement(track); removeElement(track);
}); });
} }
}); });
} }
// Try to load the value from storage // Get and set initial data
let active = this.storage.get('captions'); // The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Otherwise fall back to the default config const languages = dedupe(
if (!utils.is.boolean(active)) { Array.from(navigator.languages || navigator.userLanguage).map(language => language.split('-')[0]),
);
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
// Get language from storage, fallback to config Object.assign(this.captions, {
let language = this.storage.get('language') || this.config.captions.language; toggled: false,
if (language === 'auto') { active,
[ language ] = (navigator.language || navigator.userLanguage).split('-'); language,
} languages,
// Set language and show if active });
captions.setLanguage.call(this, language, active);
// Watch changes to textTracks and update captions menu // Watch changes to textTracks and update captions menu
if (this.isHTML5) { if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this)); on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
} }
// Update available languages in list next tick (the event must not be triggered before the listeners) // Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0); setTimeout(captions.update.bind(this), 0);
}, },
// Update available language options in settings based on tracks
update() { update() {
const tracks = captions.getTracks.call(this, true); const tracks = captions.getTracks.call(this, true);
// Get the wanted language // Get the wanted language
const { language, meta } = this.captions; const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Handle tracks (add event listener and "pseudo"-default) // Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
tracks tracks.filter(track => !meta.get(track)).forEach(track => {
.filter(track => !meta.get(track)) this.debug.log('Track added', track);
.forEach(track => { // Attempt to store if the original dom element was "default"
this.debug.log('Track added', track); meta.set(track, {
// Attempt to store if the original dom element was "default" default: track.mode === 'showing',
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));
}); });
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
} }
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); // Update language first time it matches, or if the previous matching track was removed
const firstMatch = this.language !== language && tracks.find(track => track.language === language); if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
// Update language if removed or first matching track added captions.toggle.call(this, active && languageExists);
if (trackRemoved || firstMatch) {
captions.setLanguage.call(this, language, this.config.captions.active);
} }
// Enable or disable captions based on track length // Enable or disable captions based on track length
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks)); toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list // Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
@@ -128,16 +155,70 @@ const captions = {
} }
}, },
set(index, setLanguage = true, show = true) { // Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle state
this.elements.buttons.captions.pressed = active;
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
// Disable captions if setting to -1 // Disable captions if setting to -1
if (index === -1) { if (index === -1) {
this.toggleCaptions(false); captions.toggle.call(this, false, passive);
return; return;
} }
if (!utils.is.number(index)) { if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index); this.debug.warn('Invalid caption argument', index);
return; return;
} }
@@ -149,15 +230,19 @@ const captions = {
if (this.captions.currentTrack !== index) { if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index; this.captions.currentTrack = index;
const track = captions.getCurrentTrack.call(this); const track = tracks[index];
const { language } = track || {}; const { language } = track || {};
// Store reference to node for invalidation on remove // Store reference to node for invalidation on remove
this.captions.currentTrackNode = track; this.captions.currentTrackNode = track;
// Prevent setting language in some cases, since it can violate user's intentions // Update settings menu
if (setLanguage) { controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language; this.captions.language = language;
this.storage.set({ language });
} }
// Handle Vimeo captions // Handle Vimeo captions
@@ -166,32 +251,33 @@ const captions = {
} }
// Trigger event // Trigger event
utils.dispatchEvent.call(this, this.media, 'languagechange'); triggerEvent.call(this, this.media, 'languagechange');
} }
// Show captions
captions.toggle.call(this, true, passive);
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it // If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this); captions.updateCues.call(this);
} }
// Show captions
if (show) {
this.toggleCaptions(true);
}
}, },
setLanguage(language, show = true) { // Set captions by language
if (!utils.is.string(language)) { // Used internally for the language setter with the passive option forced to false
this.debug.warn('Invalid language argument', language); setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return; return;
} }
// Normalize // Normalize
this.captions.language = language.toLowerCase(); const language = input.toLowerCase();
this.captions.language = language;
// Set currentTrack // Set currentTrack
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const track = captions.getCurrentTrack.call(this, true); const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), false, show); captions.set.call(this, tracks.indexOf(track), passive);
}, },
// Get current valid caption tracks // Get current valid caption tracks
@@ -204,34 +290,42 @@ const captions = {
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) .filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => [ .filter(track => ['captions', 'subtitles'].includes(track.kind));
'captions',
'subtitles',
].includes(track.kind));
}, },
// Get the current track for the current language // Match tracks based on languages and get the first
getCurrentTrack(fromLanguage = false) { findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0]; let track;
languages.every(language => {
track = sorted.find(track => track.language === language);
return !track; // Break iteration if there is a match
});
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
}, },
// Get UI label for track // Get UI label for track
getLabel(track) { getLabel(track) {
let currentTrack = track; let currentTrack = track;
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this); currentTrack = captions.getCurrentTrack.call(this);
} }
if (utils.is.track(currentTrack)) { if (is.track(currentTrack)) {
if (!utils.is.empty(currentTrack.label)) { if (!is.empty(currentTrack.label)) {
return currentTrack.label; return currentTrack.label;
} }
if (!utils.is.empty(currentTrack.language)) { if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase(); return track.language.toUpperCase();
} }
@@ -249,13 +343,13 @@ const captions = {
return; return;
} }
if (!utils.is.element(this.elements.captions)) { if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to'); this.debug.warn('No captions element to render to');
return; return;
} }
// Only accept array or empty input // Only accept array or empty input
if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) { if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input); this.debug.warn('updateCues: Invalid input', input);
return; return;
} }
@@ -267,7 +361,7 @@ const captions = {
const track = captions.getCurrentTrack.call(this); const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || []) cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML()) .map(cue => cue.getCueAsHTML())
.map(utils.getHTML); .map(getHTML);
} }
// Set new caption text // Set new caption text
@@ -276,13 +370,13 @@ const captions = {
if (changed) { if (changed) {
// Empty the container and create a new child element // Empty the container and create a new child element
utils.emptyElement(this.elements.captions); emptyElement(this.elements.captions);
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption)); const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content; caption.innerHTML = content;
this.elements.captions.appendChild(caption); this.elements.captions.appendChild(caption);
// Trigger event // Trigger event
utils.dispatchEvent.call(this, this.media, 'cuechange'); triggerEvent.call(this, this.media, 'cuechange');
} }
}, },
}; };
@@ -93,15 +93,7 @@ const defaults = {
// Speed default and options to display // Speed default and options to display
speed: { speed: {
selected: 1, selected: 1,
options: [ options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
],
}, },
// Keyboard shortcut settings // Keyboard shortcut settings
@@ -155,11 +147,7 @@ const defaults = {
'airplay', 'airplay',
'fullscreen', 'fullscreen',
], ],
settings: [ settings: ['captions', 'quality', 'speed'],
'captions',
'quality',
'speed',
],
// Localisation // Localisation
i18n: { i18n: {
@@ -215,7 +203,8 @@ const defaults = {
}, },
youtube: { youtube: {
sdk: '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', api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
}, },
googleIMA: { googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -13,4 +13,22 @@ export const types = {
video: 'video', video: 'video',
}; };
/**
* Get provider by URL
* @param {string} url
*/
export function getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
}
export default { providers, types }; export default { providers, types };
+228 -250
View File
File diff suppressed because it is too large Load Diff
+33 -24
View File
@@ -3,9 +3,10 @@
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing // https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ========================================================================== // ==========================================================================
import utils from './utils'; import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
const browser = utils.getBrowser(); import { on, triggerEvent } from './utils/events';
import is from './utils/is';
function onChange() { function onChange() {
if (!this.enabled) { if (!this.enabled) {
@@ -14,16 +15,16 @@ function onChange() {
// Update toggle button // Update toggle button
const button = this.player.elements.buttons.fullscreen; const button = this.player.elements.buttons.fullscreen;
if (utils.is.element(button)) { if (is.element(button)) {
button.pressed = this.active; button.pressed = this.active;
} }
// Trigger an event // Trigger an event
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container // Trap focus in container
if (!browser.isIos) { if (!browser.isIos) {
utils.trapFocus.call(this.player, this.target, this.active); trapFocus.call(this.player, this.target, this.active);
} }
} }
@@ -42,7 +43,7 @@ function toggleFallback(toggle = false) {
document.body.style.overflow = toggle ? 'hidden' : ''; document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook // Toggle class hook
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Toggle button and fire events // Toggle button and fire events
onChange.call(this); onChange.call(this);
@@ -62,15 +63,20 @@ class Fullscreen {
// Register event listeners // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target?? // TODO: Filter for target??
onChange.call(this); onChange.call(this);
}); },
);
// Fullscreen toggle on double click // Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => { on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls // Ignore double click in controls
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return; return;
} }
@@ -83,26 +89,27 @@ class Fullscreen {
// Determine if native supported // Determine if native supported
static get native() { static get native() {
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
} }
// Get the prefix for handlers // Get the prefix for handlers
static get prefix() { static get prefix() {
// No prefix // No prefix
if (utils.is.function(document.exitFullscreen)) { if (is.function(document.exitFullscreen)) {
return ''; return '';
} }
// Check for fullscreen support by vendor prefix // Check for fullscreen support by vendor prefix
let value = ''; let value = '';
const prefixes = [ const prefixes = ['webkit', 'moz', 'ms'];
'webkit',
'moz',
'ms',
];
prefixes.some(pre => { prefixes.some(pre => {
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) { if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre; value = pre;
return true; return true;
} }
@@ -135,7 +142,7 @@ class Fullscreen {
// Fallback using classname // Fallback using classname
if (!Fullscreen.native) { if (!Fullscreen.native) {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
} }
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`]; const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
@@ -145,7 +152,9 @@ class Fullscreen {
// Get target element // Get target element
get target() { get target() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
} }
// Update UI // Update UI
@@ -157,7 +166,7 @@ class Fullscreen {
} }
// Add styling hook to show button // Add styling hook to show button
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
} }
// Make an element fullscreen // Make an element fullscreen
@@ -175,7 +184,7 @@ class Fullscreen {
toggleFallback.call(this, true); toggleFallback.call(this, true);
} else if (!this.prefix) { } else if (!this.prefix) {
this.target.requestFullscreen(); this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) { } else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`](); this.target[`${this.prefix}Request${this.property}`]();
} }
} }
@@ -194,7 +203,7 @@ class Fullscreen {
toggleFallback.call(this, false); toggleFallback.call(this, false);
} else if (!this.prefix) { } else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document); (document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) { } else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`](); document[`${this.prefix}${action}${this.property}`]();
} }
+24 -62
View File
@@ -3,40 +3,28 @@
// ========================================================================== // ==========================================================================
import support from './support'; import support from './support';
import utils from './utils'; import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
const html5 = { const html5 = {
getSources() { getSources() {
if (!this.isHTML5) { if (!this.isHTML5) {
return null; return [];
} }
return this.media.querySelectorAll('source'); const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources
return sources.filter(source => support.mime.call(this, source.getAttribute('type')));
}, },
// Get quality levels // Get quality levels
getQualityOptions() { getQualityOptions() {
if (!this.isHTML5) { // Get sizes from <source> elements
return null; return html5.getSources
} .call(this)
.map(source => Number(source.getAttribute('size')))
// Get sources .filter(Boolean);
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
// If none, bail
if (utils.is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
}, },
extend() { extend() {
@@ -51,60 +39,34 @@ const html5 = {
get() { get() {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
const [source] = sources.filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(sources)) { // Return size, if match is found
return null; return source && Number(source.getAttribute('size'));
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
}, },
set(input) { set(input) {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) { // Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return; return;
} }
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state // Get current state
const { currentTime, playing } = player; const { currentTime, playing } = player;
// Set new source // Set new source
player.media.src = supported[0].getAttribute('src'); player.media.src = source.getAttribute('src');
// Restore time // Restore time
const onLoadedMetaData = () => { const onLoadedMetaData = () => {
player.currentTime = currentTime; player.currentTime = currentTime;
player.off('loadedmetadata', onLoadedMetaData);
}; };
player.on('loadedmetadata', onLoadedMetaData); player.once('loadedmetadata', onLoadedMetaData);
// Load new source // Load new source
player.media.load(); player.media.load();
@@ -115,7 +77,7 @@ const html5 = {
} }
// Trigger change event // Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input, quality: input,
}); });
}, },
@@ -130,7 +92,7 @@ const html5 = {
} }
// Remove child sources // Remove child sources
utils.removeElement(html5.getSources()); removeElement(html5.getSources.call(this));
// Set blank video src attribute // Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
+8 -9
View File
@@ -2,17 +2,19 @@
// Plyr internationalization // Plyr internationalization
// ========================================================================== // ==========================================================================
import utils from './utils'; import is from './utils/is';
import { getDeep } from './utils/objects';
import { replaceAll } from './utils/strings';
const i18n = { const i18n = {
get(key = '', config = {}) { get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config)) { if (is.empty(key) || is.empty(config)) {
return ''; return '';
} }
let string = utils.getDeep(config.i18n, key); let string = getDeep(config.i18n, key);
if (utils.is.empty(string)) { if (is.empty(string)) {
return ''; return '';
} }
@@ -21,11 +23,8 @@ const i18n = {
'{title}': config.title, '{title}': config.title,
}; };
Object.entries(replace).forEach(([ Object.entries(replace).forEach(([key, value]) => {
key, string = replaceAll(string, key, value);
value,
]) => {
string = utils.replaceAll(string, key, value);
}); });
return string; return string;
+132 -162
View File
@@ -4,10 +4,10 @@
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
// Sniff out the browser import { on, once, toggleListener, triggerEvent } from './utils/events';
const browser = utils.getBrowser(); import is from './utils/is';
class Listeners { class Listeners {
constructor(player) { constructor(player) {
@@ -32,7 +32,7 @@ class Listeners {
// If the event is bubbled from the media element // If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason // Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) { if (!is.number(code)) {
return; return;
} }
@@ -46,37 +46,16 @@ class Listeners {
// Reset on keyup // Reset on keyup
if (pressed) { if (pressed) {
// Which keycodes should we prevent default // Which keycodes should we prevent default
const preventDefault = [ const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
// Check focused element // Check focused element
// and if the focused element is not editable (e.g. text input) // and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/ // and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement(); const focused = getFocusElement();
if (utils.is.element(focused) && ( if (
focused !== this.player.elements.inputs.seek && is.element(focused) &&
utils.matches(focused, this.player.config.selectors.editable)) (focused !== this.player.elements.inputs.seek &&
matches(focused, this.player.config.selectors.editable))
) { ) {
return; return;
} }
@@ -195,41 +174,37 @@ class Listeners {
this.player.touch = true; this.player.touch = true;
// Add touch class // Add touch class
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
utils.off(document.body, 'touchstart', this.firstTouch);
} }
// Global window & document listeners // Global window & document listeners
global(toggle = true) { global(toggle = true) {
// Keyboard shortcuts // Keyboard shortcuts
if (this.player.config.keyboard.global) { if (this.player.config.keyboard.global) {
utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false);
} }
// Click anywhere closes menu // Click anywhere closes menu
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle); toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events // Detect touch by events
utils.on(document.body, 'touchstart', this.firstTouch); once.call(this.player, document.body, 'touchstart', this.firstTouch);
} }
// Container listeners // Container listeners
container() { container() {
// Keyboard shortcuts // Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false); on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false);
} }
// Detect tab focus // Detect tab focus
// Remove class on blur/focusout // Remove class on blur/focusout
utils.on(this.player.elements.container, 'focusout', event => { on.call(this.player, this.player.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false); toggleClass(event.target, this.player.config.classNames.tabFocus, false);
}); });
// Add classname to tabbed elements // Add classname to tabbed elements
utils.on(this.player.elements.container, 'keydown', event => { on.call(this.player, this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) { if (event.keyCode !== 9) {
return; return;
} }
@@ -237,59 +212,64 @@ class Listeners {
// Delay the adding of classname until the focus has changed // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
setTimeout(() => { setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true); toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0); }, 0);
}); });
// Toggle controls on mouse events and entering fullscreen // Toggle controls on mouse events and entering fullscreen
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { on.call(
const { controls } = this.player.elements; this.player,
this.player.elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => {
const { controls } = this.player.elements;
// Remove button states for fullscreen // Remove button states for fullscreen
if (event.type === 'enterfullscreen') { if (event.type === 'enterfullscreen') {
controls.pressed = false; controls.pressed = false;
controls.hover = false; controls.hover = false;
} }
// Show, then hide after a timeout unless another control event occurs // Show, then hide after a timeout unless another control event occurs
const show = [ const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
'touchstart',
'touchmove',
'mousemove',
].includes(event.type);
let delay = 0; let delay = 0;
if (show) { if (show) {
ui.toggleControls.call(this.player, true); ui.toggleControls.call(this.player, true);
// Use longer timeout for touch devices // Use longer timeout for touch devices
delay = this.player.touch ? 3000 : 2000; delay = this.player.touch ? 3000 : 2000;
} }
// Clear timer // Clear timer
clearTimeout(this.player.timers.controls); clearTimeout(this.player.timers.controls);
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
}); },
);
} }
// 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 seeked', event => controls.timeUpdate.call(this.player, event)); on.call(this.player, this.player.media, 'timeupdate seeking seeked', event =>
controls.timeUpdate.call(this.player, event),
);
// Display duration // Display duration
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event =>
controls.durationUpdate.call(this.player, event),
);
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
utils.on(this.player.media, 'loadeddata', () => { on.call(this.player, this.player.media, 'canplay', () => {
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio); toggleHidden(this.player.elements.volume, !this.player.hasAudio);
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
}); });
// Handle the media finishing // Handle the media finishing
utils.on(this.player.media, 'ended', () => { on.call(this.player, this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart // Restart
@@ -298,20 +278,28 @@ class Listeners {
}); });
// Check for buffer progress // Check for buffer progress
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); on.call(this.player, 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 => controls.updateVolume.call(this.player, event)); on.call(this.player, 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 timeupdate', event => ui.checkPlaying.call(this.player, event)); on.call(this.player, 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)); on.call(this.player, this.player.media, 'waiting canplay seeked playing', event =>
ui.checkLoading.call(this.player, event),
);
// If autoplay, then load advertisement if required // If autoplay, then load advertisement if required
// 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', () => { on.call(this.player, this.player.media, 'playing', () => {
if (!this.player.ads) { if (!this.player.ads) {
return; return;
} }
@@ -326,15 +314,15 @@ class Listeners {
// Click video // Click video
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// Re-fetch the wrapper // Re-fetch the wrapper
const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`); const wrapper = getElement.call(this.player, `.${this.player.config.classNames.video}`);
// Bail if there's no wrapper (this should never happen) // Bail if there's no wrapper (this should never happen)
if (!utils.is.element(wrapper)) { if (!is.element(wrapper)) {
return; return;
} }
// On click play, pause ore restart // On click play, pause ore restart
utils.on(wrapper, 'click', () => { on.call(this.player, wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls) // Touch devices will just show controls (if we're hiding controls)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) { if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return; return;
@@ -353,7 +341,8 @@ class Listeners {
// Disable right click // Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) { if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on( on.call(
this.player,
this.player.elements.wrapper, this.player.elements.wrapper,
'contextmenu', 'contextmenu',
event => { event => {
@@ -364,13 +353,13 @@ class Listeners {
} }
// Volume change // Volume change
utils.on(this.player.media, 'volumechange', () => { on.call(this.player, this.player.media, 'volumechange', () => {
// Save to storage // Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
}); });
// Speed change // Speed change
utils.on(this.player.media, 'ratechange', () => { on.call(this.player, this.player.media, 'ratechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'speed'); controls.updateSetting.call(this.player, 'speed');
@@ -379,49 +368,29 @@ class Listeners {
}); });
// Quality request // Quality request
utils.on(this.player.media, 'qualityrequested', event => { on.call(this.player, this.player.media, 'qualityrequested', event => {
// Save to storage // Save to storage
this.player.storage.set({ quality: event.detail.quality }); this.player.storage.set({ quality: event.detail.quality });
}); });
// Quality change // Quality change
utils.on(this.player.media, 'qualitychange', event => { on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
}); });
// Caption language change
utils.on(this.player.media, 'languagechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.player.storage.set({ language: this.player.language });
});
// Captions toggle
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.player.storage.set({ captions: this.player.captions.active });
});
// Proxy events to container // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
utils.on(this.player.media, this.player.config.events.concat([ const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' ');
'keyup', on.call(this.player, this.player.media, proxyEvents, event => {
'keydown', let { detail = {} } = event;
]).join(' '), event => {
let {detail = {}} = event;
// Get error details from media // Get error details from media
if (event.type === 'error') { if (event.type === 'error') {
detail = this.player.media.error; detail = this.player.media.error;
} }
utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail); triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail);
}); });
} }
@@ -433,7 +402,7 @@ class Listeners {
// Run default and custom handlers // Run default and custom handlers
const proxy = (event, defaultHandler, customHandlerKey) => { const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey]; const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler); const hasCustomHandler = is.function(customHandler);
let returned = true; let returned = true;
// Execute custom handler // Execute custom handler
@@ -442,33 +411,41 @@ class Listeners {
} }
// Only call default handler if not prevented in custom handler // Only call default handler if not prevented in custom handler
if (returned && utils.is.function(defaultHandler)) { if (returned && is.function(defaultHandler)) {
defaultHandler.call(this.player, event); defaultHandler.call(this.player, event);
} }
}; };
// Trigger custom and default handlers // Trigger custom and default handlers
const on = (element, type, defaultHandler, customHandlerKey, passive = true) => { const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {
const customHandler = this.player.config.listeners[customHandlerKey]; const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler); const hasCustomHandler = is.function(customHandler);
utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); on.call(
this.player,
element,
type,
event => proxy(event, defaultHandler, customHandlerKey),
passive && !hasCustomHandler,
);
}; };
// Play/pause toggle // Play/pause toggle
on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play'); Array.from(this.player.elements.buttons.play).forEach(button => {
bind(button, 'click', this.player.togglePlay, 'play');
});
// Pause // Pause
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
// Rewind // Rewind
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
// Rewind // Rewind
on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
// Mute toggle // Mute toggle
on( bind(
this.player.elements.buttons.mute, this.player.elements.buttons.mute,
'click', 'click',
() => { () => {
@@ -478,10 +455,10 @@ class Listeners {
); );
// Captions toggle // Captions toggle
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions); bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
// Fullscreen toggle // Fullscreen toggle
on( bind(
this.player.elements.buttons.fullscreen, this.player.elements.buttons.fullscreen,
'click', 'click',
() => { () => {
@@ -491,7 +468,7 @@ class Listeners {
); );
// Picture-in-Picture // Picture-in-Picture
on( bind(
this.player.elements.buttons.pip, this.player.elements.buttons.pip,
'click', 'click',
() => { () => {
@@ -501,15 +478,15 @@ class Listeners {
); );
// Airplay // Airplay
on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
// Settings menu // Settings menu
on(this.player.elements.buttons.settings, 'click', event => { bind(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event); controls.toggleMenu.call(this.player, event);
}); });
// Settings menu // Settings menu
on(this.player.elements.settings.form, 'click', event => { bind(this.player.elements.settings.form, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click // Go back to home tab on click
@@ -519,7 +496,7 @@ class Listeners {
}; };
// 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 (matches(event.target, this.player.config.selectors.inputs.language)) {
proxy( proxy(
event, event,
() => { () => {
@@ -528,7 +505,7 @@ class Listeners {
}, },
'language', 'language',
); );
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) { } else if (matches(event.target, this.player.config.selectors.inputs.quality)) {
proxy( proxy(
event, event,
() => { () => {
@@ -537,7 +514,7 @@ class Listeners {
}, },
'quality', 'quality',
); );
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) { } else if (matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy( proxy(
event, event,
() => { () => {
@@ -553,14 +530,14 @@ class Listeners {
}); });
// Set range input alternative "value", which matches the tooltip time (#954) // Set range input alternative "value", which matches the tooltip time (#954)
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
const clientRect = this.player.elements.progress.getBoundingClientRect(); const clientRect = this.player.elements.progress.getBoundingClientRect();
const percent = 100 / clientRect.width * (event.pageX - clientRect.left); const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seek-value', percent); event.currentTarget.setAttribute('seek-value', percent);
}); });
// Pause while seeking // Pause while seeking
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
const seek = event.currentTarget; const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which; const code = event.keyCode ? event.keyCode : event.which;
@@ -573,11 +550,7 @@ class Listeners {
const play = seek.hasAttribute('play-on-seeked'); const play = seek.hasAttribute('play-on-seeked');
// Done seeking // Done seeking
const done = [ const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
'mouseup',
'touchend',
'keyup',
].includes(event.type);
// If we're done seeking and it was playing, resume playback // If we're done seeking and it was playing, resume playback
if (play && done) { if (play && done) {
@@ -590,7 +563,7 @@ class Listeners {
}); });
// Seek // Seek
on( bind(
this.player.elements.inputs.seek, this.player.elements.inputs.seek,
inputEvent, inputEvent,
event => { event => {
@@ -599,7 +572,7 @@ class Listeners {
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value'); let seekTo = seek.getAttribute('seek-value');
if (utils.is.empty(seekTo)) { if (is.empty(seekTo)) {
seekTo = seek.value; seekTo = seek.value;
} }
@@ -612,8 +585,8 @@ class Listeners {
// Current time invert // Current time invert
// Only if one time element is used for both currentTime and duration // Only if one time element is used for both currentTime and duration
if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) { if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
on(this.player.elements.display.currentTime, 'click', () => { bind(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start // Do nothing if we're at the start
if (this.player.currentTime === 0) { if (this.player.currentTime === 0) {
return; return;
@@ -626,7 +599,7 @@ class Listeners {
} }
// Volume // Volume
on( bind(
this.player.elements.inputs.volume, this.player.elements.inputs.volume,
inputEvent, inputEvent,
event => { event => {
@@ -637,33 +610,32 @@ class Listeners {
// Polyfill for lower fill in <input type="range"> for webkit // Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) { if (browser.isWebkit) {
on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => { Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => {
controls.updateRangeFill.call(this.player, event.target); bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
}); });
} }
// Seek tooltip // Seek tooltip
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event)); bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event =>
controls.updateSeekTooltip.call(this.player, event),
);
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
on(this.player.elements.controls, 'mouseenter mouseleave', event => { bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
}); });
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = [ this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
'mousedown',
'touchstart',
].includes(event.type);
}); });
// Focus in/out on controls // Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', event => { bind(this.player.elements.controls, 'focusin focusout', event => {
const { config, elements, timers } = this.player; const { config, elements, timers } = this.player;
// Skip transition to prevent focus from scrolling the parent element // Skip transition to prevent focus from scrolling the parent element
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
// Toggle // Toggle
ui.toggleControls.call(this.player, event.type === 'focusin'); ui.toggleControls.call(this.player, event.type === 'focusin');
@@ -672,7 +644,7 @@ class Listeners {
if (event.type === 'focusin') { if (event.type === 'focusin') {
// Restore transition // Restore transition
setTimeout(() => { setTimeout(() => {
utils.toggleClass(elements.controls, config.classNames.noTransition, false); toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0); }, 0);
// Delay a little more for keyboard users // Delay a little more for keyboard users
@@ -686,7 +658,7 @@ class Listeners {
}); });
// Mouse wheel for volume // Mouse wheel for volume
on( bind(
this.player.elements.inputs.volume, this.player.elements.inputs.volume,
'wheel', 'wheel',
event => { event => {
@@ -719,7 +691,10 @@ class Listeners {
} }
// Don't break page scrolling at max and min // Don't break page scrolling at max and min
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) { if (
(direction === 1 && this.player.media.volume < 1) ||
(direction === -1 && this.player.media.volume > 0)
) {
event.preventDefault(); event.preventDefault();
} }
}, },
@@ -727,11 +702,6 @@ class Listeners {
false, false,
); );
} }
// Reset on destroy
clear() {
this.global(false);
}
} }
export default Listeners; export default Listeners;
+12 -21
View File
@@ -5,7 +5,7 @@
import html5 from './html5'; import html5 from './html5';
import vimeo from './plugins/vimeo'; import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube'; import youtube from './plugins/youtube';
import utils from './utils'; import { createElement, toggleClass, wrap } from './utils/elements';
const media = { const media = {
// Setup media // Setup media
@@ -17,50 +17,41 @@ const media = {
} }
// Add type class // Add type class
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class // Add provider class
utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds // Add video class for embeds
// This will require changes if audio embeds are added // This will require changes if audio embeds are added
if (this.isEmbed) { if (this.isEmbed) {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
} }
// Inject the player wrapper // Inject the player wrapper
if (this.isVideo) { if (this.isVideo) {
// Create the wrapper div // Create the wrapper div
this.elements.wrapper = utils.createElement('div', { this.elements.wrapper = createElement('div', {
class: this.config.classNames.video, class: this.config.classNames.video,
}); });
// Wrap the video in a container // Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper); wrap(this.media, this.elements.wrapper);
// Faux poster container // Faux poster container
this.elements.poster = utils.createElement('div', { this.elements.poster = createElement('div', {
class: this.config.classNames.poster, class: this.config.classNames.poster,
}); });
this.elements.wrapper.appendChild(this.elements.poster); this.elements.wrapper.appendChild(this.elements.poster);
} }
if (this.isEmbed) { if (this.isHTML5) {
switch (this.provider) {
case 'youtube':
youtube.setup.call(this);
break;
case 'vimeo':
vimeo.setup.call(this);
break;
default:
break;
}
} else if (this.isHTML5) {
html5.extend.call(this); html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
} }
}, },
}; };
+26 -18
View File
@@ -7,7 +7,12 @@
/* global google */ /* global google */
import i18n from '../i18n'; import i18n from '../i18n';
import utils from '../utils'; import { createElement } from './../utils/elements';
import { triggerEvent } from './../utils/events';
import is from './../utils/is';
import loadScript from './../utils/loadScript';
import { formatTime } from './../utils/time';
import { buildUrlParams } from './../utils/urls';
class Ads { class Ads {
/** /**
@@ -44,7 +49,7 @@ class Ads {
} }
get enabled() { get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId); return this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId);
} }
/** /**
@@ -53,9 +58,8 @@ class Ads {
load() { load() {
if (this.enabled) { if (this.enabled) {
// 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 (!is.object(window.google) || !is.object(window.google.ima)) {
utils loadScript(this.player.config.urls.googleIMA.sdk)
.loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => { .then(() => {
this.ready(); this.ready();
}) })
@@ -103,7 +107,7 @@ class Ads {
const base = 'https://go.aniview.com/api/adserver6/vast/'; const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`; return `${base}?${buildUrlParams(params)}`;
} }
/** /**
@@ -116,7 +120,7 @@ class Ads {
*/ */
setupIMA() { setupIMA() {
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = utils.createElement('div', { this.elements.container = createElement('div', {
class: this.player.config.classNames.ads, class: this.player.config.classNames.ads,
}); });
this.player.elements.container.appendChild(this.elements.container); this.player.elements.container.appendChild(this.elements.container);
@@ -146,7 +150,11 @@ class Ads {
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false); this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
event => this.onAdsManagerLoaded(event),
false,
);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads // Request video ads
@@ -184,7 +192,7 @@ class Ads {
} }
const update = () => { const update = () => {
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0)); const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`; const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label); this.elements.container.setAttribute('data-badge-text', label);
}; };
@@ -212,14 +220,14 @@ class Ads {
this.cuePoints = this.manager.getCuePoints(); this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available // Add advertisement cue's within the time line if available
if (!utils.is.empty(this.cuePoints)) { if (!is.empty(this.cuePoints)) {
this.cuePoints.forEach(cuePoint => { this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) { if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress; const seekElement = this.player.elements.progress;
if (utils.is.element(seekElement)) { if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint; const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', { const cue = createElement('span', {
class: this.player.config.classNames.cues, class: this.player.config.classNames.cues,
}); });
@@ -266,7 +274,7 @@ class Ads {
// Proxy event // Proxy event
const dispatchEvent = type => { const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`; const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
utils.dispatchEvent.call(this.player, this.player.media, event); triggerEvent.call(this.player, this.player.media, event);
}; };
switch (event.type) { switch (event.type) {
@@ -393,7 +401,7 @@ class Ads {
this.player.on('seeked', () => { this.player.on('seeked', () => {
const seekedTime = this.player.currentTime; const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) { if (is.empty(this.cuePoints)) {
return; return;
} }
@@ -530,9 +538,9 @@ class Ads {
trigger(event, ...args) { trigger(event, ...args) {
const handlers = this.events[event]; const handlers = this.events[event];
if (utils.is.array(handlers)) { if (is.array(handlers)) {
handlers.forEach(handler => { handlers.forEach(handler => {
if (utils.is.function(handler)) { if (is.function(handler)) {
handler.apply(this, args); handler.apply(this, args);
} }
}); });
@@ -546,7 +554,7 @@ class Ads {
* @return {Ads} * @return {Ads}
*/ */
on(event, callback) { on(event, callback) {
if (!utils.is.array(this.events[event])) { if (!is.array(this.events[event])) {
this.events[event] = []; this.events[event] = [];
} }
@@ -577,7 +585,7 @@ class Ads {
* @param {string} from * @param {string} from
*/ */
clearSafetyTimer(from) { clearSafetyTimer(from) {
if (!utils.is.nullOrUndefined(this.safetyTimer)) { if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`); this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer); clearTimeout(this.safetyTimer);
+68 -42
View File
@@ -5,7 +5,34 @@
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'; import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch';
import is from './../utils/is';
import loadScript from './../utils/loadScript';
import { format, stripHTML } from './../utils/strings';
import { buildUrlParams } from './../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Get aspect ratio for dimensions
function getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
}
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
@@ -14,22 +41,21 @@ function assurePlaybackState(play) {
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
const vimeo = { const vimeo = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set intial ratio // Set intial ratio
vimeo.setAspectRatio.call(this); vimeo.setAspectRatio.call(this);
// Load the API if not already // Load the API if not already
if (!utils.is.object(window.Vimeo)) { if (!is.object(window.Vimeo)) {
utils loadScript(this.config.urls.vimeo.sdk)
.loadScript(this.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(this); vimeo.ready.call(this);
}) })
@@ -44,8 +70,8 @@ const vimeo = {
// Set aspect ratio // Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI // For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
setAspectRatio(input) { setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
const padding = 100 / ratio[0] * ratio[1]; const padding = 100 / x * y;
this.elements.wrapper.style.paddingBottom = `${padding}%`; this.elements.wrapper.style.paddingBottom = `${padding}%`;
if (this.supported.ui) { if (this.supported.ui) {
@@ -73,34 +99,37 @@ const vimeo = {
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative, playsinline: !this.config.fullscreen.iosNative,
}; };
const params = utils.buildUrlParams(options); const params = buildUrlParams(options);
// Get the source URL or ID // Get the source URL or ID
let source = player.media.getAttribute('src'); let source = player.media.getAttribute('src');
// Get from <div> if needed // Get from <div> if needed
if (utils.is.empty(source)) { if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id); source = player.media.getAttribute(player.config.attributes.embed.id);
} }
const id = utils.parseVimeoId(source); const id = parseId(source);
// Build an iframe // Build an iframe
const iframe = utils.createElement('iframe'); const iframe = createElement('iframe');
const src = utils.format(player.config.urls.vimeo.iframe, id, params); const src = 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');
// Get poster, if already set
const { poster } = player;
// Inject the package // Inject the package
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer }); const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe); wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media); player.media = replaceElement(wrapper, player.media);
// Get poster image // Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => { fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) { if (is.empty(response)) {
return; return;
} }
@@ -111,7 +140,7 @@ const vimeo = {
url.pathname = `${url.pathname.split('_')[0]}.jpg`; url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster // Set and show poster
ui.setPoster.call(player, url.href); ui.setPoster.call(player, url.href).catch(() => {});
}); });
// Setup instance // Setup instance
@@ -160,7 +189,7 @@ const vimeo = {
// Set seeking state and trigger event // Set seeking state and trigger event
media.seeking = true; media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking'); triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete // If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0)) Promise.resolve(restorePause && embed.setVolume(0))
@@ -187,7 +216,7 @@ const vimeo = {
.setPlaybackRate(input) .setPlaybackRate(input)
.then(() => { .then(() => {
speed = input; speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}) })
.catch(error => { .catch(error => {
// Hide menu item (and menu if empty) // Hide menu item (and menu if empty)
@@ -207,7 +236,7 @@ const vimeo = {
set(input) { set(input) {
player.embed.setVolume(input).then(() => { player.embed.setVolume(input).then(() => {
volume = input; volume = input;
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}); });
}, },
}); });
@@ -219,11 +248,11 @@ const vimeo = {
return muted; return muted;
}, },
set(input) { set(input) {
const toggle = utils.is.boolean(input) ? input : false; const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle; muted = toggle;
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}); });
}, },
}); });
@@ -235,7 +264,7 @@ const vimeo = {
return loop; return loop;
}, },
set(input) { set(input) {
const toggle = utils.is.boolean(input) ? input : player.config.loop.active; const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => { player.embed.setLoop(toggle).then(() => {
loop = toggle; loop = toggle;
@@ -268,11 +297,8 @@ const vimeo = {
}); });
// Set aspect ratio based on video size // Set aspect ratio based on video size
Promise.all([ Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
player.embed.getVideoWidth(), const ratio = getAspectRatio(dimensions[0], dimensions[1]);
player.embed.getVideoHeight(),
]).then(dimensions => {
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, ratio); vimeo.setAspectRatio.call(this, ratio);
}); });
@@ -290,13 +316,13 @@ const vimeo = {
// Get current time // Get current time
player.embed.getCurrentTime().then(value => { player.embed.getCurrentTime().then(value => {
currentTime = value; currentTime = value;
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}); });
// Get duration // Get duration
player.embed.getDuration().then(value => { player.embed.getDuration().then(value => {
player.media.duration = value; player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
}); });
// Get captions // Get captions
@@ -306,7 +332,7 @@ const vimeo = {
}); });
player.embed.on('cuechange', ({ cues = [] }) => { player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map(cue => utils.stripHTML(cue.text)); const strippedCues = cues.map(cue => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues); captions.updateCues.call(player, strippedCues);
}); });
@@ -315,11 +341,11 @@ const vimeo = {
player.embed.getPaused().then(paused => { player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused); assurePlaybackState.call(player, !paused);
if (!paused) { if (!paused) {
utils.dispatchEvent.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
} }
}); });
if (utils.is.element(player.embed.element) && player.supported.ui) { if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element; const frame = player.embed.element;
// Fix keyboard focus issues // Fix keyboard focus issues
@@ -330,7 +356,7 @@ const vimeo = {
player.embed.on('play', () => { player.embed.on('play', () => {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
}); });
player.embed.on('pause', () => { player.embed.on('pause', () => {
@@ -340,16 +366,16 @@ const vimeo = {
player.embed.on('timeupdate', data => { player.embed.on('timeupdate', data => {
player.media.seeking = false; player.media.seeking = false;
currentTime = data.seconds; currentTime = data.seconds;
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}); });
player.embed.on('progress', data => { player.embed.on('progress', data => {
player.media.buffered = data.percent; player.media.buffered = data.percent;
utils.dispatchEvent.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
// Check all loaded // Check all loaded
if (parseInt(data.percent, 10) === 1) { if (parseInt(data.percent, 10) === 1) {
utils.dispatchEvent.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'canplaythrough');
} }
// Get duration as if we do it before load, it gives an incorrect value // Get duration as if we do it before load, it gives an incorrect value
@@ -357,24 +383,24 @@ const vimeo = {
player.embed.getDuration().then(value => { player.embed.getDuration().then(value => {
if (value !== player.media.duration) { if (value !== player.media.duration) {
player.media.duration = value; player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
} }
}); });
}); });
player.embed.on('seeked', () => { player.embed.on('seeked', () => {
player.media.seeking = false; player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked'); triggerEvent.call(player, player.media, 'seeked');
}); });
player.embed.on('ended', () => { player.embed.on('ended', () => {
player.media.paused = true; player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'ended');
}); });
player.embed.on('error', detail => { player.embed.on('error', detail => {
player.media.error = detail; player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error'); triggerEvent.call(player, player.media, 'error');
}); });
// Rebuild UI // Rebuild UI
+103 -139
View File
@@ -4,64 +4,54 @@
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils'; import { dedupe } from './../utils/arrays';
import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch';
import is from './../utils/is';
import loadImage from './../utils/loadImage';
import loadScript from './../utils/loadScript';
import { format, generateId } from './../utils/strings';
// Parse YouTube ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Standardise YouTube quality unit // Standardise YouTube quality unit
function mapQualityUnit(input) { function mapQualityUnit(input) {
switch (input) { const qualities = {
case 'hd2160': hd2160: 2160,
return 2160; hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
case 2160: const entry = Object.entries(qualities).find(entry => entry.includes(input));
return 'hd2160';
case 'hd1440': if (entry) {
return 1440; // Get the match corresponding to the input
return entry.find(value => value !== input);
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
} }
return 'default';
} }
function mapQualityUnits(levels) { function mapQualityUnits(levels) {
if (utils.is.empty(levels)) { if (is.empty(levels)) {
return levels; return levels;
} }
return utils.dedupe(levels.map(level => mapQualityUnit(level))); return dedupe(levels.map(level => mapQualityUnit(level)));
} }
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
@@ -71,24 +61,24 @@ function assurePlaybackState(play) {
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio // Set aspect ratio
youtube.setAspectRatio.call(this); youtube.setAspectRatio.call(this);
// Setup API // Setup API
if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Load the API // Load the API
utils.loadScript(this.config.urls.youtube.sdk).catch(error => { 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);
}); });
@@ -115,10 +105,10 @@ const youtube = {
// Try via undocumented API method first // Try via undocumented API method first
// This method disappears now and then though... // This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709 // https://github.com/sampotts/plyr/issues/709
if (utils.is.function(this.embed.getVideoData)) { if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData(); const { title } = this.embed.getVideoData();
if (utils.is.empty(title)) { if (is.empty(title)) {
this.config.title = title; this.config.title = title;
ui.setTitle.call(this); ui.setTitle.call(this);
return; return;
@@ -127,13 +117,12 @@ 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 (is.string(key) && !is.empty(key)) {
const url = utils.format(this.config.urls.youtube.api, videoId, key); const url = format(this.config.urls.youtube.api, videoId, key);
utils fetch(url)
.fetch(url)
.then(result => { .then(result => {
if (utils.is.object(result)) { if (is.object(result)) {
this.config.title = result.items[0].snippet.title; this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this); ui.setTitle.call(this);
} }
@@ -154,7 +143,7 @@ const youtube = {
// Ignore already setup (race condition) // Ignore already setup (race condition)
const currentId = player.media.getAttribute('id'); const currentId = player.media.getAttribute('id');
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return; return;
} }
@@ -162,30 +151,36 @@ const youtube = {
let source = player.media.getAttribute('src'); let source = player.media.getAttribute('src');
// Get from <div> if needed // Get from <div> if needed
if (utils.is.empty(source)) { if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id); source = player.media.getAttribute(this.config.attributes.embed.id);
} }
// Replace the <iframe> with a <div> due to YouTube API issues // Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(source); const videoId = parseId(source);
const id = utils.generateId(player.provider); const id = generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
// Set poster image // Get poster, if already set
const { poster } = player;
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; 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) // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src)) .then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => { .then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters) // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) { if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover'; player.elements.poster.style.backgroundSize = 'cover';
} }
}); })
.catch(() => {});
// Setup instance // Setup instance
// https://developers.google.com/youtube/iframe_api_reference // https://developers.google.com/youtube/iframe_api_reference
@@ -211,49 +206,26 @@ const youtube = {
}, },
events: { events: {
onError(event) { onError(event) {
// If we've already fired an error, don't do it again // YouTube may fire onError twice, so only handle it once
// YouTube fires onError twice if (!player.media.error) {
if (utils.is.object(player.media.error)) { const code = event.data;
return; // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
} }
const detail = {
code: event.data,
};
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
switch (event.data) {
case 2:
detail.message =
'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
break;
case 5:
detail.message =
'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
break;
case 100:
detail.message =
'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
break;
case 101:
case 150:
detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
break;
default:
detail.message = 'An unknown error occured';
break;
}
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
}, },
onPlaybackQualityChange() { onPlaybackQualityChange() {
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality, quality: player.media.quality,
}); });
}, },
@@ -264,7 +236,7 @@ const youtube = {
// Get current speed // Get current speed
player.media.playbackRate = instance.getPlaybackRate(); player.media.playbackRate = instance.getPlaybackRate();
utils.dispatchEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}, },
onReady(event) { onReady(event) {
// Get the instance // Get the instance
@@ -305,7 +277,7 @@ const youtube = {
// Set seeking state and trigger event // Set seeking state and trigger event
player.media.seeking = true; player.media.seeking = true;
utils.dispatchEvent.call(player, player.media, 'seeking'); triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent // Seek after events sent
instance.seekTo(time); instance.seekTo(time);
@@ -328,15 +300,7 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality()); return mapQualityUnit(instance.getPlaybackQuality());
}, },
set(input) { set(input) {
const quality = input; instance.setPlaybackQuality(mapQualityUnit(input));
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality,
});
}, },
}); });
@@ -349,7 +313,7 @@ const youtube = {
set(input) { set(input) {
volume = input; volume = input;
instance.setVolume(volume * 100); instance.setVolume(volume * 100);
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}, },
}); });
@@ -360,10 +324,10 @@ const youtube = {
return muted; return muted;
}, },
set(input) { set(input) {
const toggle = utils.is.boolean(input) ? input : muted; const toggle = is.boolean(input) ? input : muted;
muted = toggle; muted = toggle;
instance[toggle ? 'mute' : 'unMute'](); instance[toggle ? 'mute' : 'unMute']();
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}, },
}); });
@@ -389,8 +353,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1); player.media.setAttribute('tabindex', -1);
} }
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
// Reset timer // Reset timer
clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
@@ -402,7 +366,7 @@ const youtube = {
// Trigger progress only when we actually buffer something // Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
utils.dispatchEvent.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
} }
// Set last buffer point // Set last buffer point
@@ -413,7 +377,7 @@ const youtube = {
clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
// Trigger event // Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'canplaythrough');
} }
}, 200); }, 200);
@@ -427,15 +391,12 @@ const youtube = {
// Reset timer // Reset timer
clearInterval(player.timers.playing); clearInterval(player.timers.playing);
const seeked = player.media.seeking && [ const seeked = player.media.seeking && [1, 2].includes(event.data);
1,
2,
].includes(event.data);
if (seeked) { if (seeked) {
// Unset seeking and fire seeked event // Unset seeking and fire seeked event
player.media.seeking = false; player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked'); triggerEvent.call(player, player.media, 'seeked');
} }
// Handle events // Handle events
@@ -448,11 +409,11 @@ const youtube = {
switch (event.data) { switch (event.data) {
case -1: case -1:
// Update scrubber // Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube // Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction(); player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
break; break;
@@ -465,7 +426,7 @@ const youtube = {
instance.stopVideo(); instance.stopVideo();
instance.playVideo(); instance.playVideo();
} else { } else {
utils.dispatchEvent.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'ended');
} }
break; break;
@@ -477,11 +438,11 @@ const youtube = {
} else { } else {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress // Poll to get playback progress
player.timers.playing = setInterval(() => { player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}, 50); }, 50);
// Check duration again due to YouTube bug // Check duration again due to YouTube bug
@@ -489,11 +450,14 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690 // https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) { if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration(); player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
} }
// Get quality // Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); controls.setQualityMenu.call(
player,
mapQualityUnits(instance.getAvailableQualityLevels()),
);
} }
break; break;
@@ -511,7 +475,7 @@ const youtube = {
break; break;
} }
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data, code: event.data,
}); });
}, },
+109 -135
View File
@@ -6,9 +6,10 @@
// ========================================================================== // ==========================================================================
import captions from './captions'; import captions from './captions';
import defaults from './config/defaults';
import { getProviderByUrl, providers, types } from './config/types';
import Console from './console'; import Console from './console';
import controls from './controls'; 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 media from './media'; import media from './media';
@@ -16,9 +17,14 @@ import Ads from './plugins/ads';
import source from './source'; import source from './source';
import Storage from './storage'; import Storage from './storage';
import support from './support'; import support from './support';
import { providers, types } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import { cloneDeep, extend } from './utils/objects';
import { parseUrl } from './utils/urls';
// Private properties // Private properties
// TODO: Use a WeakMap for private globals // TODO: Use a WeakMap for private globals
@@ -41,18 +47,18 @@ class Plyr {
this.media = target; this.media = target;
// String selector passed // String selector passed
if (utils.is.string(this.media)) { if (is.string(this.media)) {
this.media = document.querySelectorAll(this.media); this.media = document.querySelectorAll(this.media);
} }
// jQuery, NodeList or Array passed, use first element // jQuery, NodeList or Array passed, use first element
if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) { if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {
// eslint-disable-next-line // eslint-disable-next-line
this.media = this.media[0]; this.media = this.media[0];
} }
// Set config // Set config
this.config = utils.extend( this.config = extend(
{}, {},
defaults, defaults,
Plyr.defaults, Plyr.defaults,
@@ -108,7 +114,7 @@ class Plyr {
this.debug.log('Support', support); this.debug.log('Support', support);
// We need an element to setup // We need an element to setup
if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) { if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
this.debug.error('Setup failed: no suitable element passed'); this.debug.error('Setup failed: no suitable element passed');
return; return;
} }
@@ -144,7 +150,6 @@ class Plyr {
// Embed properties // Embed properties
let iframe = null; let iframe = null;
let url = null; let url = null;
let params = null;
// Different setup based on type // Different setup based on type
switch (type) { switch (type) {
@@ -153,10 +158,10 @@ class Plyr {
iframe = this.media.querySelector('iframe'); iframe = this.media.querySelector('iframe');
// <iframe> type // <iframe> type
if (utils.is.element(iframe)) { if (is.element(iframe)) {
// Detect provider // Detect provider
url = iframe.getAttribute('src'); url = parseUrl(iframe.getAttribute('src'));
this.provider = utils.getProviderByUrl(url); this.provider = getProviderByUrl(url.toString());
// Rework elements // Rework elements
this.elements.container = this.media; this.elements.container = this.media;
@@ -166,24 +171,20 @@ class Plyr {
this.elements.container.className = ''; this.elements.container.className = '';
// Get attributes from URL and set config // Get attributes from URL and set config
params = utils.getUrlParams(url); if (url.searchParams.length) {
if (!utils.is.empty(params)) { const truthy = ['1', 'true'];
const truthy = [
'1',
'true',
];
if (truthy.includes(params.autoplay)) { if (truthy.includes(url.searchParams.get('autoplay'))) {
this.config.autoplay = true; this.config.autoplay = true;
} }
if (truthy.includes(params.loop)) { if (truthy.includes(url.searchParams.get('loop'))) {
this.config.loop.active = true; this.config.loop.active = true;
} }
// TODO: replace fullscreen.iosNative with this playsinline config option // TODO: replace fullscreen.iosNative with this playsinline config option
// YouTube requires the playsinline in the URL // YouTube requires the playsinline in the URL
if (this.isYouTube) { if (this.isYouTube) {
this.config.playsinline = truthy.includes(params.playsinline); this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
} else { } else {
this.config.playsinline = true; this.config.playsinline = true;
} }
@@ -197,7 +198,7 @@ class Plyr {
} }
// Unsupported or missing provider // Unsupported or missing provider
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
this.debug.error('Setup failed: Invalid provider'); this.debug.error('Setup failed: Invalid provider');
return; return;
} }
@@ -245,6 +246,8 @@ class Plyr {
return; return;
} }
this.eventListeners = [];
// Create listeners // Create listeners
this.listeners = new Listeners(this); this.listeners = new Listeners(this);
@@ -255,9 +258,9 @@ class Plyr {
this.media.plyr = this; this.media.plyr = this;
// Wrap media // Wrap media
if (!utils.is.element(this.elements.container)) { if (!is.element(this.elements.container)) {
this.elements.container = utils.createElement('div'); this.elements.container = createElement('div');
utils.wrap(this.media, this.elements.container); wrap(this.media, this.elements.container);
} }
// Add style hook // Add style hook
@@ -268,7 +271,7 @@ class Plyr {
// Listen for events if debugging // Listen for events if debugging
if (this.config.debug) { if (this.config.debug) {
utils.on(this.elements.container, this.config.events.join(' '), event => { on.call(this, this.elements.container, this.config.events.join(' '), event => {
this.debug.log(`event: ${event.type}`); this.debug.log(`event: ${event.type}`);
}); });
} }
@@ -327,7 +330,7 @@ class Plyr {
* Play the media, or play the advertisement (if they are not blocked) * Play the media, or play the advertisement (if they are not blocked)
*/ */
play() { play() {
if (!utils.is.function(this.media.play)) { if (!is.function(this.media.play)) {
return null; return null;
} }
@@ -339,7 +342,7 @@ class Plyr {
* Pause the media * Pause the media
*/ */
pause() { pause() {
if (!this.playing || !utils.is.function(this.media.pause)) { if (!this.playing || !is.function(this.media.pause)) {
return; return;
} }
@@ -380,7 +383,7 @@ class Plyr {
*/ */
togglePlay(input) { togglePlay(input) {
// Toggle based on current state if nothing passed // Toggle based on current state if nothing passed
const toggle = utils.is.boolean(input) ? input : !this.playing; const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) { if (toggle) {
this.play(); this.play();
@@ -396,7 +399,7 @@ class Plyr {
if (this.isHTML5) { if (this.isHTML5) {
this.pause(); this.pause();
this.restart(); this.restart();
} else if (utils.is.function(this.media.stop)) { } else if (is.function(this.media.stop)) {
this.media.stop(); this.media.stop();
} }
} }
@@ -413,7 +416,7 @@ class Plyr {
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/ */
rewind(seekTime) { rewind(seekTime) {
this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime); this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
} }
/** /**
@@ -421,7 +424,7 @@ class Plyr {
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/ */
forward(seekTime) { forward(seekTime) {
this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime); this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
} }
/** /**
@@ -435,7 +438,7 @@ class Plyr {
} }
// Validate input // Validate input
const inputIsValid = utils.is.number(input) && input > 0; const inputIsValid = is.number(input) && input > 0;
// Set // Set
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
@@ -458,7 +461,7 @@ class Plyr {
const { buffered } = this.media; const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1 // YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) { if (is.number(buffered)) {
return buffered; return buffered;
} }
@@ -502,17 +505,17 @@ class Plyr {
const max = 1; const max = 1;
const min = 0; const min = 0;
if (utils.is.string(volume)) { if (is.string(volume)) {
volume = Number(volume); volume = Number(volume);
} }
// Load volume from storage if no value specified // Load volume from storage if no value specified
if (!utils.is.number(volume)) { if (!is.number(volume)) {
volume = this.storage.get('volume'); volume = this.storage.get('volume');
} }
// Use config if all else fails // Use config if all else fails
if (!utils.is.number(volume)) { if (!is.number(volume)) {
({ volume } = this.config); ({ volume } = this.config);
} }
@@ -532,7 +535,7 @@ class Plyr {
this.media.volume = volume; this.media.volume = volume;
// If muted, and we're increasing volume manually, reset muted state // If muted, and we're increasing volume manually, reset muted state
if (!utils.is.empty(value) && this.muted && volume > 0) { if (!is.empty(value) && this.muted && volume > 0) {
this.muted = false; this.muted = false;
} }
} }
@@ -550,7 +553,7 @@ class Plyr {
*/ */
increaseVolume(step) { increaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume; const volume = this.media.muted ? 0 : this.volume;
this.volume = volume + (utils.is.number(step) ? step : 1); this.volume = volume + (is.number(step) ? step : 1);
} }
/** /**
@@ -559,7 +562,7 @@ class Plyr {
*/ */
decreaseVolume(step) { decreaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume; const volume = this.media.muted ? 0 : this.volume;
this.volume = volume - (utils.is.number(step) ? step : 1); this.volume = volume - (is.number(step) ? step : 1);
} }
/** /**
@@ -570,12 +573,12 @@ class Plyr {
let toggle = mute; let toggle = mute;
// Load muted state from storage // Load muted state from storage
if (!utils.is.boolean(toggle)) { if (!is.boolean(toggle)) {
toggle = this.storage.get('muted'); toggle = this.storage.get('muted');
} }
// Use config if all else fails // Use config if all else fails
if (!utils.is.boolean(toggle)) { if (!is.boolean(toggle)) {
toggle = this.config.muted; toggle = this.config.muted;
} }
@@ -621,15 +624,15 @@ class Plyr {
set speed(input) { set speed(input) {
let speed = null; let speed = null;
if (utils.is.number(input)) { if (is.number(input)) {
speed = input; speed = input;
} }
if (!utils.is.number(speed)) { if (!is.number(speed)) {
speed = this.storage.get('speed'); speed = this.storage.get('speed');
} }
if (!utils.is.number(speed)) { if (!is.number(speed)) {
speed = this.config.speed.selected; speed = this.config.speed.selected;
} }
@@ -666,36 +669,31 @@ class Plyr {
* @param {number} input - Quality level * @param {number} input - Quality level
*/ */
set quality(input) { set quality(input) {
let quality = null; const config = this.config.quality;
const options = this.options.quality;
if (!utils.is.empty(input)) { if (!options.length) {
quality = Number(input);
}
if (!utils.is.number(quality)) {
quality = this.storage.get('quality');
}
if (!utils.is.number(quality)) {
quality = this.config.quality.selected;
}
if (!utils.is.number(quality)) {
quality = this.config.quality.default;
}
if (!this.options.quality.length) {
return; return;
} }
if (!this.options.quality.includes(quality)) { let quality = [
const closest = utils.closest(this.options.quality, quality); !is.empty(input) && Number(input),
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`); this.storage.get('quality'),
quality = closest; config.selected,
config.default,
].find(is.number);
if (!options.includes(quality)) {
const value = closest(options, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
quality = value;
} }
// Trigger request event
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality });
// Update config // Update config
this.config.quality.selected = quality; config.selected = quality;
// Set quality // Set quality
this.media.quality = quality; this.media.quality = quality;
@@ -714,7 +712,7 @@ class Plyr {
* @param {boolean} input - Whether to loop or not * @param {boolean} input - Whether to loop or not
*/ */
set loop(input) { set loop(input) {
const toggle = utils.is.boolean(input) ? input : this.config.loop.active; const toggle = is.boolean(input) ? input : this.config.loop.active;
this.config.loop.active = toggle; this.config.loop.active = toggle;
this.media.loop = toggle; this.media.loop = toggle;
@@ -794,7 +792,7 @@ class Plyr {
return; return;
} }
ui.setPoster.call(this, input); ui.setPoster.call(this, input, false).catch(() => {});
} }
/** /**
@@ -813,7 +811,7 @@ class Plyr {
* @param {boolean} input - Whether to autoplay or not * @param {boolean} input - Whether to autoplay or not
*/ */
set autoplay(input) { set autoplay(input) {
const toggle = utils.is.boolean(input) ? input : this.config.autoplay; const toggle = is.boolean(input) ? input : this.config.autoplay;
this.config.autoplay = toggle; this.config.autoplay = toggle;
} }
@@ -829,41 +827,23 @@ class Plyr {
* @param {boolean} input - Whether to enable captions * @param {boolean} input - Whether to enable captions
*/ */
toggleCaptions(input) { toggleCaptions(input) {
// If there's no full support captions.toggle.call(this, input, false);
if (!this.supported.ui) {
return;
} }
// If the method is called without parameter, toggle based on current value
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Toggle state
this.elements.buttons.captions.pressed = active;
// Add class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
// Update state and trigger event
if (active !== this.captions.active) {
this.captions.active = active;
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
}
}
/** /**
* Set the caption track by index * Set the caption track by index
* @param {number} - Caption index * @param {number} - Caption index
*/ */
set currentTrack(input) { set currentTrack(input) {
captions.set.call(this, input); captions.set.call(this, input, false);
} }
/** /**
* Get the current caption track index (-1 if disabled) * Get the current caption track index (-1 if disabled)
*/ */
get currentTrack() { get currentTrack() {
const { active, currentTrack } = this.captions; const { toggled, currentTrack } = this.captions;
return active ? currentTrack : -1; return toggled ? currentTrack : -1;
} }
/** /**
@@ -872,7 +852,7 @@ class Plyr {
* @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) {
captions.setLanguage.call(this, input); captions.setLanguage.call(this, input, false);
} }
/** /**
@@ -899,7 +879,7 @@ class Plyr {
} }
// Toggle based on current state if not passed // Toggle based on current state if not passed
const toggle = utils.is.boolean(input) ? input : this.pip === states.inline; const toggle = is.boolean(input) ? input : this.pip === states.inline;
// Toggle based on current state // Toggle based on current state
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
@@ -935,22 +915,22 @@ class Plyr {
// Don't toggle if missing UI support or if it's audio // Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) { if (this.supported.ui && !this.isAudio) {
// Get state before change // Get state before change
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls); const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Negate the argument if not undefined since adding the class to hides the controls // Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle; const force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state // Apply and get updated state
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force); const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Close menu // Close menu
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false); controls.toggleMenu.call(this, false);
} }
// Trigger event on change // Trigger event on change
if (hiding !== isHidden) { if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown'; const eventName = hiding ? 'controlshidden' : 'controlsshown';
utils.dispatchEvent.call(this, this.media, eventName); triggerEvent.call(this, this.media, eventName);
} }
return !hiding; return !hiding;
} }
@@ -963,16 +943,23 @@ class Plyr {
* @param {function} callback - Callback for when event occurs * @param {function} callback - Callback for when event occurs
*/ */
on(event, callback) { on(event, callback) {
utils.on(this.elements.container, event, callback); on.call(this, this.elements.container, event, callback);
}
/**
* Add event listeners once
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
once(event, callback) {
once.call(this, this.elements.container, event, callback);
} }
/** /**
* Remove event listeners * Remove event listeners
* @param {string} event - Event type * @param {string} event - Event type
* @param {function} callback - Callback for when event occurs * @param {function} callback - Callback for when event occurs
*/ */
off(event, callback) { off(event, callback) {
utils.off(this.elements.container, event, callback); off(this.elements.container, event, callback);
} }
/** /**
@@ -998,10 +985,10 @@ class Plyr {
if (soft) { if (soft) {
if (Object.keys(this.elements).length) { if (Object.keys(this.elements).length) {
// Remove elements // Remove elements
utils.removeElement(this.elements.buttons.play); removeElement(this.elements.buttons.play);
utils.removeElement(this.elements.captions); removeElement(this.elements.captions);
utils.removeElement(this.elements.controls); removeElement(this.elements.controls);
utils.removeElement(this.elements.wrapper); removeElement(this.elements.wrapper);
// Clear for GC // Clear for GC
this.elements.buttons.play = null; this.elements.buttons.play = null;
@@ -1011,21 +998,21 @@ class Plyr {
} }
// Callback // Callback
if (utils.is.function(callback)) { if (is.function(callback)) {
callback(); callback();
} }
} else { } else {
// Unbind listeners // Unbind listeners
this.listeners.clear(); unbindListeners.call(this);
// Replace the container with the original element provided // Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container); replaceElement(this.elements.original, this.elements.container);
// Event // Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true); triggerEvent.call(this, this.elements.original, 'destroyed', true);
// Callback // Callback
if (utils.is.function(callback)) { if (is.function(callback)) {
callback.call(this.elements.original); callback.call(this.elements.original);
} }
@@ -1043,10 +1030,8 @@ class Plyr {
// Stop playback // Stop playback
this.stop(); this.stop();
// Type specific stuff // Provider specific stuff
switch (`${this.provider}:${this.type}`) { if (this.isHTML5) {
case 'html5:video':
case 'html5:audio':
// Clear timeout // Clear timeout
clearTimeout(this.timers.loading); clearTimeout(this.timers.loading);
@@ -1055,25 +1040,19 @@ class Plyr {
// Clean up // Clean up
done(); done();
} else if (this.isYouTube) {
break;
case 'youtube:video':
// Clear timers // Clear timers
clearInterval(this.timers.buffering); clearInterval(this.timers.buffering);
clearInterval(this.timers.playing); clearInterval(this.timers.playing);
// Destroy YouTube API // Destroy YouTube API
if (this.embed !== null && utils.is.function(this.embed.destroy)) { if (this.embed !== null && is.function(this.embed.destroy)) {
this.embed.destroy(); this.embed.destroy();
} }
// Clean up // Clean up
done(); done();
} else if (this.isVimeo) {
break;
case 'vimeo:video':
// Destroy Vimeo API // Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors) // then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) { if (this.embed !== null) {
@@ -1082,11 +1061,6 @@ class Plyr {
// Vimeo does not always return // Vimeo does not always return
setTimeout(done, 200); setTimeout(done, 200);
break;
default:
break;
} }
} }
@@ -1114,7 +1088,7 @@ class Plyr {
* @param {string} [id] - Unique ID * @param {string} [id] - Unique ID
*/ */
static loadSprite(url, id) { static loadSprite(url, id) {
return utils.loadSprite(url, id); return loadSprite(url, id);
} }
/** /**
@@ -1125,15 +1099,15 @@ class Plyr {
static setup(selector, options = {}) { static setup(selector, options = {}) {
let targets = null; let targets = null;
if (utils.is.string(selector)) { if (is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector)); targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) { } else if (is.nodeList(selector)) {
targets = Array.from(selector); targets = Array.from(selector);
} else if (utils.is.array(selector)) { } else if (is.array(selector)) {
targets = selector.filter(utils.is.element); targets = selector.filter(is.element);
} }
if (utils.is.empty(targets)) { if (is.empty(targets)) {
return null; return null;
} }
@@ -1141,6 +1115,6 @@ class Plyr {
} }
} }
Plyr.defaults = utils.cloneDeep(defaults); Plyr.defaults = cloneDeep(defaults);
export default Plyr; export default Plyr;
+26 -37
View File
@@ -2,23 +2,25 @@
// Plyr source update // Plyr source update
// ========================================================================== // ==========================================================================
import { providers } from './config/types';
import html5 from './html5'; import html5 from './html5';
import media from './media'; import media from './media';
import support from './support'; import support from './support';
import { providers } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import { createElement, insertElement, removeElement } from './utils/elements';
import is from './utils/is';
import { getDeep } from './utils/objects';
const source = { const source = {
// Add elements to HTML5 media (source, tracks, etc) // Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) { insertElements(type, attributes) {
if (utils.is.string(attributes)) { if (is.string(attributes)) {
utils.insertElement(type, this.media, { insertElement(type, this.media, {
src: attributes, src: attributes,
}); });
} else if (utils.is.array(attributes)) { } else if (is.array(attributes)) {
attributes.forEach(attribute => { attributes.forEach(attribute => {
utils.insertElement(type, this.media, attribute); insertElement(type, this.media, attribute);
}); });
} }
}, },
@@ -26,7 +28,7 @@ const source = {
// Update source // Update source
// Sources are not checked for support so be careful // Sources are not checked for support so be careful
change(input) { change(input) {
if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) { if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format'); this.debug.warn('Invalid source format');
return; return;
} }
@@ -42,47 +44,34 @@ const source = {
this.options.quality = []; this.options.quality = [];
// Remove elements // Remove elements
utils.removeElement(this.media); removeElement(this.media);
this.media = null; this.media = null;
// Reset class name // Reset class name
if (utils.is.element(this.elements.container)) { if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class'); this.elements.container.removeAttribute('class');
} }
// Set the type and provider // Set the type and provider
this.type = input.type; const { sources, type } = input;
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5; const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Check for support Object.assign(this, {
this.supported = support.check(this.type, this.provider, this.config.playsinline); provider,
type,
// Create new markup // Check for support
switch (`${this.provider}:${this.type}`) { supported: support.check(type, provider, this.config.playsinline),
case 'html5:video': // Create new element
this.media = utils.createElement('video'); media: createElement(tagName, attributes),
break; });
case 'html5:audio':
this.media = utils.createElement('audio');
break;
case 'youtube:video':
case 'vimeo:video':
this.media = utils.createElement('div', {
src: input.sources[0].src,
});
break;
default:
break;
}
// Inject the new element // Inject the new element
this.elements.container.appendChild(this.media); this.elements.container.appendChild(this.media);
// Autoplay the new source? // Autoplay the new source?
if (utils.is.boolean(input.autoplay)) { if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay; this.config.autoplay = input.autoplay;
} }
@@ -94,7 +83,7 @@ const source = {
if (this.config.autoplay) { if (this.config.autoplay) {
this.media.setAttribute('autoplay', ''); this.media.setAttribute('autoplay', '');
} }
if (!utils.is.empty(input.poster)) { if (!is.empty(input.poster)) {
this.poster = input.poster; this.poster = input.poster;
} }
if (this.config.loop.active) { if (this.config.loop.active) {
@@ -113,7 +102,7 @@ const source = {
// Set new sources for html5 // Set new sources for html5
if (this.isHTML5) { if (this.isHTML5) {
source.insertElements.call(this, 'source', input.sources); source.insertElements.call(this, 'source', sources);
} }
// Set video title // Set video title
+7 -6
View File
@@ -2,7 +2,8 @@
// Plyr storage // Plyr storage
// ========================================================================== // ==========================================================================
import utils from './utils'; import is from './utils/is';
import { extend } from './utils/objects';
class Storage { class Storage {
constructor(player) { constructor(player) {
@@ -37,13 +38,13 @@ class Storage {
const store = window.localStorage.getItem(this.key); const store = window.localStorage.getItem(this.key);
if (utils.is.empty(store)) { if (is.empty(store)) {
return null; return null;
} }
const json = JSON.parse(store); const json = JSON.parse(store);
return utils.is.string(key) && key.length ? json[key] : json; return is.string(key) && key.length ? json[key] : json;
} }
set(object) { set(object) {
@@ -53,7 +54,7 @@ class Storage {
} }
// Can only store objectst // Can only store objectst
if (!utils.is.object(object)) { if (!is.object(object)) {
return; return;
} }
@@ -61,12 +62,12 @@ class Storage {
let storage = this.get(); let storage = this.get();
// Default to empty object // Default to empty object
if (utils.is.empty(storage)) { if (is.empty(storage)) {
storage = {}; storage = {};
} }
// Update the working copy of the values // Update the working copy of the values
utils.extend(storage, object); extend(storage, object);
// Update storage // Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage)); window.localStorage.setItem(this.key, JSON.stringify(storage));
+38 -101
View File
@@ -2,7 +2,19 @@
// Plyr support checks // Plyr support checks
// ========================================================================== // ==========================================================================
import utils from './utils'; import { transitionEndEvent } from './utils/animation';
import browser from './utils/browser';
import { createElement } from './utils/elements';
import is from './utils/is';
// Default codecs for checking mimetype support
const defaultCodecs = {
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora',
};
// Check for feature support // Check for feature support
const support = { const support = {
@@ -13,32 +25,9 @@ const support = {
// Check for support // Check for support
// Basic functionality vs full UI // Basic functionality vs full UI
check(type, provider, playsinline) { check(type, provider, playsinline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const canPlayInline = browser.isIPhone && playsinline && support.playsinline; const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5';
switch (`${provider}:${type}`) { const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
case 'html5:video':
api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
case 'html5:audio':
api = support.audio;
ui = api && support.rangeInput;
break;
case 'youtube:video':
case 'vimeo:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
default:
api = support.audio && support.video;
ui = api && support.rangeInput;
}
return { return {
api, api,
@@ -48,14 +37,11 @@ const support = {
// Picture-in-picture support // Picture-in-picture support
// Safari only currently // Safari only currently
pip: (() => { pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
const browser = utils.getBrowser();
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
})(),
// Airplay support // Airplay support
// Safari only currently // Safari only currently
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent), airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support // Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/ // https://webkit.org/blog/6784/new-video-policies-for-ios/
@@ -64,83 +50,34 @@ const support = {
// Check for mime type support against a player instance // Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html // Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html // Related: http://www.leanbackplayer.com/test/h5mt.html
mime(type) { mime(inputType) {
const { media } = this; const [mediaType] = inputType.split('/');
if (!this.isHTML5 || mediaType !== this.type) {
try {
// Bail if no checking function
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
return false;
}
// Check directly if codecs specified
if (type.includes('codecs=')) {
return media.canPlayType(type).replace(/no/, '');
}
// Type specific checks
if (this.isVideo) {
switch (type) {
case 'video/webm':
return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
case 'video/mp4':
return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
case 'video/ogg':
return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
default:
return false;
}
} else if (this.isAudio) {
switch (type) {
case 'audio/mpeg':
return media.canPlayType('audio/mpeg;').replace(/no/, '');
case 'audio/ogg':
return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
case 'audio/wav':
return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
default:
return false;
}
}
} catch (e) {
return false; return false;
} }
// If we got this far, we're stuffed let type;
return false; if (inputType && inputType.includes('codecs=')) {
// Use input directly
type = inputType;
} else if (inputType === 'audio/mpeg') {
// Skip codec
type = 'audio/mpeg;';
} else if (inputType in defaultCodecs) {
// Use codec
type = `${inputType}; codecs="${defaultCodecs[inputType]}"`;
}
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (err) {
return false;
}
}, },
// Check for textTracks support // Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'), textTracks: 'textTracks' in document.createElement('video'),
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
passiveListeners: (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})(),
// <input type="range"> Sliders // <input type="range"> Sliders
rangeInput: (() => { rangeInput: (() => {
const range = document.createElement('input'); const range = document.createElement('input');
@@ -153,7 +90,7 @@ const support = {
touch: 'ontouchstart' in document.documentElement, touch: 'ontouchstart' in document.documentElement,
// Detect transitions support // Detect transitions support
transitions: utils.transitionEndEvent !== false, transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting // Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/ // https://webkit.org/blog/7551/responsive-design-for-motion/
+68 -52
View File
@@ -6,15 +6,16 @@ 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 support from './support';
import utils from './utils'; import browser from './utils/browser';
import { getElement, toggleClass } from './utils/elements';
// Sniff out the browser import { ready, triggerEvent } from './utils/events';
const browser = utils.getBrowser(); import is from './utils/is';
import loadImage from './utils/loadImage';
const ui = { const ui = {
addStyleHook() { addStyleHook() {
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
}, },
// Toggle native HTML5 media controls // Toggle native HTML5 media controls
@@ -44,7 +45,7 @@ const ui = {
} }
// Inject custom controls if not present // Inject custom controls if not present
if (!utils.is.element(this.elements.controls)) { if (!is.element(this.elements.controls)) {
// Inject custom controls // Inject custom controls
controls.inject.call(this); controls.inject.call(this);
@@ -85,31 +86,35 @@ const ui = {
ui.checkPlaying.call(this); ui.checkPlaying.call(this);
// Check for picture-in-picture support // Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for airplay support // Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class // Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class // Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls // Ready for API calls
this.ready = true; this.ready = true;
// Ready event at end of execution stack // Ready event at end of execution stack
setTimeout(() => { setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready'); triggerEvent.call(this, this.media, 'ready');
}, 0); }, 0);
// 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 // 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) { if (this.poster) {
ui.setPoster.call(this, this.poster); ui.setPoster.call(this, this.poster, false).catch(() => {});
} }
// Manually set the duration if user has overridden it. // Manually set the duration if user has overridden it.
@@ -125,12 +130,12 @@ const ui = {
let label = i18n.get('play', this.config); let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label // If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) { if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`; label += `, ${this.config.title}`;
} }
// If there's a play button, set label // If there's a play button, set label
if (utils.is.nodeList(this.elements.buttons.play)) { if (is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => { Array.from(this.elements.buttons.play).forEach(button => {
button.setAttribute('aria-label', label); button.setAttribute('aria-label', label);
}); });
@@ -139,14 +144,14 @@ const ui = {
// Set iframe title // Set iframe title
// https://github.com/sampotts/plyr/issues/124 // https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) { if (this.isEmbed) {
const iframe = utils.getElement.call(this, 'iframe'); const iframe = getElement.call(this, 'iframe');
if (!utils.is.element(iframe)) { if (!is.element(iframe)) {
return; return;
} }
// Default to media type // Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config); const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title)); iframe.setAttribute('title', format.replace('{title}', title));
@@ -155,44 +160,58 @@ const ui = {
// Toggle poster // Toggle poster
togglePoster(enable) { togglePoster(enable) {
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
}, },
// Set the poster image (async) // Set the poster image (async)
setPoster(poster) { // Used internally for the poster setter, with the passive option forced to false
// Set property regardless of validity setPoster(poster, passive = true) {
this.media.setAttribute('poster', poster); // Don't override if call is passive
if (passive && this.poster) {
// Bail if element is missing return Promise.reject(new Error('Poster already set'));
if (!utils.is.element(this.elements.poster)) {
return Promise.reject();
} }
// Load the image, and set poster if successful // Set property synchronously to respect the call order
const loadPromise = utils.loadImage(poster).then(() => { this.media.setAttribute('poster', poster);
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) // Wait until ui is ready
loadPromise.catch(() => ui.togglePoster.call(this, false)); return (
ready
// Return the promise so the caller can use it as well .call(this)
return loadPromise; // Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
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;
})
);
}, },
// Check playing state // Check playing state
checkPlaying(event) { checkPlaying(event) {
// Class hooks // Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused); toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state // Set state
Array.from(this.elements.buttons.play).forEach(target => { Array.from(this.elements.buttons.play).forEach(target => {
@@ -200,7 +219,7 @@ const ui = {
}); });
// Only update controls on non timeupdate events // Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') { if (is.event(event) && event.type === 'timeupdate') {
return; return;
} }
@@ -210,10 +229,7 @@ const ui = {
// Check if media is loading // Check if media is loading
checkLoading(event) { checkLoading(event) {
this.loading = [ this.loading = ['stalled', 'waiting'].includes(event.type);
'stalled',
'waiting',
].includes(event.type);
// Clear timer // Clear timer
clearTimeout(this.timers.loading); clearTimeout(this.timers.loading);
@@ -221,7 +237,7 @@ const ui = {
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => { this.timers.loading = setTimeout(() => {
// Update progress bar loading class state // Update progress bar loading class state
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update controls visibility // Update controls visibility
ui.toggleControls.call(this); ui.toggleControls.call(this);
-853
View File
@@ -1,853 +0,0 @@
// ==========================================================================
// Plyr utils
// ==========================================================================
import loadjs from 'loadjs';
import Storage from './storage';
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
is: {
object(input) {
return utils.getConstructor(input) === Object;
},
number(input) {
return utils.getConstructor(input) === Number && !Number.isNaN(input);
},
string(input) {
return utils.getConstructor(input) === String;
},
boolean(input) {
return utils.getConstructor(input) === Boolean;
},
function(input) {
return utils.getConstructor(input) === Function;
},
array(input) {
return !utils.is.nullOrUndefined(input) && Array.isArray(input);
},
weakMap(input) {
return utils.is.instanceof(input, WeakMap);
},
nodeList(input) {
return utils.is.instanceof(input, NodeList);
},
element(input) {
return utils.is.instanceof(input, Element);
},
textNode(input) {
return utils.getConstructor(input) === Text;
},
event(input) {
return utils.is.instanceof(input, Event);
},
cue(input) {
return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue);
},
track(input) {
return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind));
},
url(input) {
return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
},
nullOrUndefined(input) {
return input === null || typeof input === 'undefined';
},
empty(input) {
return (
utils.is.nullOrUndefined(input) ||
((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) ||
(utils.is.object(input) && !Object.keys(input).length)
);
},
instanceof(input, constructor) {
return Boolean(input && constructor && input instanceof constructor);
},
},
getConstructor(input) {
return !utils.is.nullOrUndefined(input) ? input.constructor : null;
},
// Unfortunately, due to mixed support, UA sniffing is required
getBrowser() {
return {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
},
// Fetch wrapper
// Using XHR to avoid issues with older browsers
fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
},
// 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
loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
},
// Load an external SVG sprite
loadSprite(url, id) {
if (!utils.is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = utils.is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
utils.toggleHidden(container, true);
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
utils
.fetch(url)
.then(result => {
if (utils.is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
},
// Generate a random ID
generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
},
// Wrap an element
wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
},
// Create a DocumentFragment
createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (utils.is.object(attributes)) {
utils.setAttributes(element, attributes);
}
// Add text node
if (utils.is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
},
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Insert a DocumentFragment
insertElement(type, parent, attributes, text) {
// Inject the new <element>
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove element(s)
removeElement(element) {
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
},
// Remove all child elements
emptyElement(element) {
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
},
// Replace element
replaceElement(newChild, oldChild) {
if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
},
// Set attributes
setAttributes(element, attributes) {
if (!utils.is.element(element) || utils.is.empty(attributes)) {
return;
}
Object.entries(attributes).forEach(([
key,
value,
]) => {
element.setAttribute(key, value);
});
},
// Get an attribute object from a string selector
getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!utils.is.string(sel) || utils.is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (utils.is.object(existing) && utils.is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
},
// Toggle hidden
toggleHidden(element, hidden) {
if (!utils.is.element(element)) {
return;
}
let hide = hidden;
if (!utils.is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass(element, className, force) {
if (utils.is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return null;
},
// Has class name
hasClass(element, className) {
return utils.is.element(element) && element.classList.contains(className);
},
// Element matches selector
matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return matches.call(element, selector);
},
// Find all elements
getElements(selector) {
return this.elements.container.querySelectorAll(selector);
},
// Find a single element
getElement(selector) {
return this.elements.container.querySelector(selector);
},
// Get the focused element
getFocusElement() {
let focused = document.activeElement;
if (!focused || focused === document.body) {
focused = null;
} else {
focused = document.querySelector(':focus');
}
return focused;
},
// Trap focus inside container
trapFocus(element = null, toggle = false) {
if (!utils.is.element(element)) {
return;
}
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = utils.getFocusElement();
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
if (toggle) {
utils.on(this.elements.container, 'keydown', trap, false);
} else {
utils.off(this.elements.container, 'keydown', trap, false);
}
},
// Toggle event listener
toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no elemetns, event, or callback
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
return;
}
// If a nodelist is passed, call itself on each node
if (utils.is.nodeList(elements) || utils.is.array(elements)) {
// Create listener for each node
Array.from(elements).forEach(element => {
if (element instanceof Node) {
utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
}
});
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (support.passiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
},
// Bind event handler
on(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, true, passive, capture);
},
// Unbind event handler
off(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, false, passive, capture);
},
// Trigger event
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!utils.is.element(element) || utils.is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
});
// Dispatch the event
element.dispatchEvent(event);
},
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
},
// Get percentage
getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
},
// Time helpers
getHours(value) {
return parseInt((value / 60 / 60) % 60, 10);
},
getMinutes(value) {
return parseInt((value / 60) % 60, 10);
},
getSeconds(value) {
return parseInt(value % 60, 10);
},
// Format time to UI friendly string
formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!utils.is.number(time)) {
return utils.formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = utils.getHours(time);
const mins = utils.getMinutes(time);
const secs = utils.getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
},
// Replace all occurances of a string in a string
replaceAll(input = '', find = '', replace = '') {
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
},
// Convert to title case
toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
},
// Convert string to pascalCase
toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = utils.replaceAll(string, '-', ' ');
// Convert snake case
string = utils.replaceAll(string, '_', ' ');
// Convert to title case
string = utils.toTitleCase(string);
// Convert to pascal case
return utils.replaceAll(string, ' ', '');
},
// Convert string to pascalCase
toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = utils.toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
},
// Deep extend destination object with N more objects
extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!utils.is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (utils.is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
utils.extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return utils.extend(target, ...sources);
},
// Remove duplicates in an array
dedupe(array) {
if (!utils.is.array(array)) {
return array;
}
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
closest(array, value) {
if (!utils.is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
},
// Get the provider for a given URL
getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
},
// Parse YouTube ID from URL
parseYouTubeId(url) {
if (utils.is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Parse Vimeo ID from URL
parseVimeoId(url) {
if (utils.is.empty(url)) {
return null;
}
if (utils.is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Convert a URL to a location object
parseUrl(url) {
const parser = document.createElement('a');
parser.href = url;
return parser;
},
// Get URL query parameters
getUrlParams(input) {
let search = input;
// Parse URL if needed
if (input.startsWith('http://') || input.startsWith('https://')) {
({ search } = utils.parseUrl(input));
}
if (utils.is.empty(search)) {
return null;
}
const hashes = search.slice(search.indexOf('?') + 1).split('&');
return hashes.reduce((params, hash) => {
const [
key,
val,
] = hash.split('=');
return Object.assign(params, { [key]: decodeURIComponent(val) });
}, {});
},
// Convert object to URL parameters
buildUrlParams(input) {
if (!utils.is.object(input)) {
return '';
}
return Object.keys(input)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`)
.join('&');
},
// Remove HTML from a string
stripHTML(source) {
const fragment = document.createDocumentFragment();
const element = document.createElement('div');
fragment.appendChild(element);
element.innerHTML = source;
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
getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
},
// Get the transition end event
get transitionEndEvent() {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return utils.is.string(type) ? events[type] : false;
},
// Force repaint of element
repaint(element) {
setTimeout(() => {
utils.toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
utils.toggleHidden(element, false);
}, 0);
},
};
export default utils;
+30
View File
@@ -0,0 +1,30 @@
// ==========================================================================
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false;
})();
// Force repaint of element
export function repaint(element) {
setTimeout(() => {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
}, 0);
}
+23
View File
@@ -0,0 +1,23 @@
// ==========================================================================
// Array utils
// ==========================================================================
import is from './is';
// Remove duplicates in an array
export function dedupe(array) {
if (!is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
}
// Get the closest value in an array
export function closest(array, value) {
if (!is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
+13
View File
@@ -0,0 +1,13 @@
// ==========================================================================
// Browser sniffing
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
export default browser;
+285
View File
@@ -0,0 +1,285 @@
// ==========================================================================
// Element utils
// ==========================================================================
import { toggleListener } from './events';
import is from './is';
// Wrap an element
export function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
}
// Set attributes
export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) {
return;
}
// Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes)
.filter(([, value]) => !is.nullOrUndefined(value))
.forEach(([key, value]) => element.setAttribute(key, value));
}
// Create a DocumentFragment
export function createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (is.object(attributes)) {
setAttributes(element, attributes);
}
// Add text node
if (is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
}
// Inaert an element after another
export function insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
}
// Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) {
// Inject the new <element>
parent.appendChild(createElement(type, attributes, text));
}
// Remove element(s)
export function removeElement(element) {
if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement);
return;
}
if (!is.element(element) || !is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
}
// Remove all child elements
export function emptyElement(element) {
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
}
// Replace element
export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
// Get an attribute object from a string selector
export function getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (is.object(existing) && is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
}
// Toggle hidden
export function toggleHidden(element, hidden) {
if (!is.element(element)) {
return;
}
let hide = hidden;
if (!is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) {
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return null;
}
// Has class name
export function hasClass(element, className) {
return is.element(element) && element.classList.contains(className);
}
// Element matches selector
export function matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
return matches.call(element, selector);
}
// Find all elements
export function getElements(selector) {
return this.elements.container.querySelectorAll(selector);
}
// Find a single element
export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// Get the focused element
export function getFocusElement() {
let focused = document.activeElement;
if (!focused || focused === document.body) {
focused = null;
} else {
focused = document.querySelector(':focus');
}
return focused;
}
// Trap focus inside container
export function trapFocus(element = null, toggle = false) {
if (!is.element(element)) {
return;
}
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = getFocusElement();
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
+120
View File
@@ -0,0 +1,120 @@
// ==========================================================================
// Event utils
// ==========================================================================
import is from './is';
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
const supportsPassiveListeners = (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})();
// Toggle event listener
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
}
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
}
// Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, true, passive, capture);
}
// Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, false, passive, capture);
}
// Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) {
function onceCallback(...args) {
off(element, events, onceCallback, passive, capture);
callback.apply(this, args);
}
toggleListener.call(this, element, events, onceCallback, true, passive, capture);
}
// Trigger event
export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!is.element(element) || is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
});
// Dispatch the event
element.dispatchEvent(event);
}
// Unbind all cached event listeners
export function unbindListeners() {
if (this && this.eventListeners) {
this.eventListeners.forEach(item => {
const { element, type, callback, options } = item;
element.removeEventListener(type, callback, options);
});
this.eventListeners = [];
}
}
// Run method when / if player is ready
export function ready() {
return new Promise(
resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)),
).then(() => {});
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Fetch wrapper
// Using XHR to avoid issues with older browsers
// ==========================================================================
export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
}
+67
View File
@@ -0,0 +1,67 @@
// ==========================================================================
// Type checking utils
// ==========================================================================
const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
const is = {
object(input) {
return getConstructor(input) === Object;
},
number(input) {
return getConstructor(input) === Number && !Number.isNaN(input);
},
string(input) {
return getConstructor(input) === String;
},
boolean(input) {
return getConstructor(input) === Boolean;
},
function(input) {
return getConstructor(input) === Function;
},
array(input) {
return !is.nullOrUndefined(input) && Array.isArray(input);
},
weakMap(input) {
return instanceOf(input, WeakMap);
},
nodeList(input) {
return instanceOf(input, NodeList);
},
element(input) {
return instanceOf(input, Element);
},
textNode(input) {
return getConstructor(input) === Text;
},
event(input) {
return instanceOf(input, Event);
},
cue(input) {
return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
},
track(input) {
return instanceOf(input, TextTrack) || (!is.nullOrUndefined(input) && is.string(input.kind));
},
url(input) {
return (
!is.nullOrUndefined(input) &&
/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input)
);
},
nullOrUndefined(input) {
return input === null || typeof input === 'undefined';
},
empty(input) {
return (
is.nullOrUndefined(input) ||
((is.string(input) || is.array(input) || is.nodeList(input)) && !input.length) ||
(is.object(input) && !Object.keys(input).length)
);
},
};
export default is;
+19
View File
@@ -0,0 +1,19 @@
// ==========================================================================
// 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
// ==========================================================================
export default function 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 });
});
}
+14
View File
@@ -0,0 +1,14 @@
// ==========================================================================
// Load an external script
// ==========================================================================
import loadjs from 'loadjs';
export default function loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
}
+75
View File
@@ -0,0 +1,75 @@
// ==========================================================================
// Sprite loader
// ==========================================================================
import Storage from './../storage';
import is from './is';
// Load an external SVG sprite
export default function loadSprite(url, id) {
if (!is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Object utils
// ==========================================================================
import is from './is';
// Clone nested objects
export function cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
}
// Get a nested value in an object
export function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
}
// Deep extend destination object with N more objects
export function extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return extend(target, ...sources);
}
+85
View File
@@ -0,0 +1,85 @@
// ==========================================================================
// String utils
// ==========================================================================
import is from './is';
// Generate a random ID
export function generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
}
// Format string
export function format(input, ...args) {
if (is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
}
// Get percentage
export function getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
}
// Replace all occurances of a string in a string
export function replaceAll(input = '', find = '', replace = '') {
return input.replace(
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
replace.toString(),
);
}
// Convert to title case
export function toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
}
// Convert string to pascalCase
export function toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = replaceAll(string, '-', ' ');
// Convert snake case
string = replaceAll(string, '_', ' ');
// Convert to title case
string = toTitleCase(string);
// Convert to pascal case
return replaceAll(string, ' ', '');
}
// Convert string to pascalCase
export function toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
}
// Remove HTML from a string
export function stripHTML(source) {
const fragment = document.createDocumentFragment();
const element = document.createElement('div');
fragment.appendChild(element);
element.innerHTML = source;
return fragment.firstChild.innerText;
}
// Like outerHTML, but also works for DocumentFragment
export function getHTML(element) {
const wrapper = document.createElement('div');
wrapper.appendChild(element);
return wrapper.innerHTML;
}
+36
View File
@@ -0,0 +1,36 @@
// ==========================================================================
// Time utils
// ==========================================================================
import is from './is';
// Time helpers
export const getHours = value => parseInt((value / 60 / 60) % 60, 10);
export const getMinutes = value => parseInt((value / 60) % 60, 10);
export const getSeconds = value => parseInt(value % 60, 10);
// Format time to UI friendly string
export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
const secs = getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}
+39
View File
@@ -0,0 +1,39 @@
// ==========================================================================
// URL utils
// ==========================================================================
import is from './is';
/**
* Parse a string to a URL object
* @param {string} input - the URL to be parsed
* @param {boolean} safe - failsafe parsing
*/
export function parseUrl(input, safe = true) {
let url = input;
if (safe) {
const parser = document.createElement('a');
parser.href = url;
url = parser.href;
}
try {
return new URL(url);
} catch (e) {
return null;
}
}
// Convert object to URLSearchParams
export function buildUrlParams(input) {
const params = new URLSearchParams();
if (is.object(input)) {
Object.entries(input).forEach(([key, value]) => {
params.set(key, value);
});
}
return params;
}
+52 -37
View File
@@ -102,6 +102,14 @@
call-me-maybe "^1.0.1" call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0" glob-to-regexp "^0.3.0"
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
"@types/node@*":
version "10.3.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3"
abbrev@1: abbrev@1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -2616,13 +2624,13 @@ gulp-autoprefixer@^5.0.0:
through2 "^2.0.0" through2 "^2.0.0"
vinyl-sourcemaps-apply "^0.2.0" vinyl-sourcemaps-apply "^0.2.0"
gulp-better-rollup@^3.1.0: gulp-better-rollup@^3.2.1:
version "3.1.0" version "3.2.1"
resolved "https://registry.yarnpkg.com/gulp-better-rollup/-/gulp-better-rollup-3.1.0.tgz#b226ba0c672882075472158b82d22ba9976d4ecb" resolved "https://registry.yarnpkg.com/gulp-better-rollup/-/gulp-better-rollup-3.2.1.tgz#c6fc26c19cd11475c58a4be97e8a7e00f36b3ac2"
dependencies: dependencies:
lodash.camelcase "^4.3.0" lodash.camelcase "^4.3.0"
plugin-error "^0.1.2" plugin-error "^1.0.1"
rollup ">=0.48 <0.57" rollup "^0.60.2"
vinyl "^2.1.0" vinyl "^2.1.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
@@ -2678,9 +2686,9 @@ gulp-postcss@^7.0.1:
postcss-load-config "^1.2.0" postcss-load-config "^1.2.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
gulp-rename@^1.2.3: gulp-rename@^1.3.0:
version "1.2.3" version "1.3.0"
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.2.3.tgz#37b75298e9d3e6c0fe9ac4eac13ce3be5434646b" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.3.0.tgz#2e789d8f563ab0c924eeb62967576f37ff4cb826"
gulp-replace@^1.0.0: gulp-replace@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -3878,6 +3886,10 @@ lodash@>=3.10.0, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, l
version "4.17.5" version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.17.10:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
lodash@~1.0.1: lodash@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551"
@@ -4723,9 +4735,9 @@ 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.23.6: postcss-html@^0.28.0:
version "0.23.7" version "0.28.0"
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.23.7.tgz#47146c15e21b9c00746c40115dcff8270c439f32" resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.28.0.tgz#3dd0f5b5d7f886b8181bf844396d43a7898162cb"
dependencies: dependencies:
htmlparser2 "^3.9.2" htmlparser2 "^3.9.2"
@@ -4735,9 +4747,9 @@ postcss-less@^1.1.0:
dependencies: dependencies:
postcss "^5.2.16" postcss "^5.2.16"
postcss-less@^1.1.5: postcss-less@^2.0.0:
version "1.1.5" version "2.0.0"
resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-1.1.5.tgz#a6f0ce180cf3797eeee1d4adc0e9e6d6db665609" resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-2.0.0.tgz#5d190b8e057ca446d60fe2e2587ad791c9029fb8"
dependencies: dependencies:
postcss "^5.2.16" postcss "^5.2.16"
@@ -4764,9 +4776,9 @@ postcss-load-plugins@^2.3.0:
cosmiconfig "^2.1.1" cosmiconfig "^2.1.1"
object-assign "^4.1.0" object-assign "^4.1.0"
postcss-markdown@^0.23.6: postcss-markdown@^0.28.0:
version "0.23.7" version "0.28.0"
resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.23.7.tgz#7e3a398794295c425e51e4f0abdee6d13ad3d134" resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.28.0.tgz#99d1c4e74967af9e9c98acb2e2b66df4b3c6ed86"
dependencies: dependencies:
remark "^9.0.0" remark "^9.0.0"
unist-util-find-all-after "^1.0.2" unist-util-find-all-after "^1.0.2"
@@ -4837,9 +4849,9 @@ 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: postcss-syntax@^0.28.0:
version "0.9.1" version "0.28.0"
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.9.1.tgz#5dbd90af1631ab8805b8f594bef2c2e8002d3758" resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.28.0.tgz#e17572a7dcf5388f0c9b68232d2dad48fa7f0b12"
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"
@@ -4988,9 +5000,9 @@ randomatic@^1.1.3:
is-number "^3.0.0" is-number "^3.0.0"
kind-of "^4.0.0" kind-of "^4.0.0"
raven-js@^3.26.1: raven-js@^3.26.2:
version "3.26.1" version "3.26.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.1.tgz#13f78804f2bed524a7283382e1bca7ab423950a3" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.2.tgz#9153af2416e96ccf4e0b9cbc6c90c34dda0d7e88"
rc@^1.0.1, rc@^1.1.6: rc@^1.0.1, rc@^1.1.6:
version "1.2.6" version "1.2.6"
@@ -5466,9 +5478,12 @@ rollup-pluginutils@^2.0.1:
estree-walker "^0.3.0" estree-walker "^0.3.0"
micromatch "^2.3.11" micromatch "^2.3.11"
"rollup@>=0.48 <0.57": rollup@^0.60.2:
version "0.56.5" version "0.60.7"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.56.5.tgz#40fe3cf0cd1659d469baad11f4d5b6336c14ce84" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.60.7.tgz#2b62ef9306f719b1ab85a7814b3e6596ac51fae8"
dependencies:
"@types/estree" "0.0.39"
"@types/node" "*"
run-async@^2.2.0: run-async@^2.2.0:
version "2.3.0" version "2.3.0"
@@ -5923,11 +5938,11 @@ 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.1.0: stylelint-scss@^3.1.2:
version "3.1.0" version "3.1.2"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.0.tgz#aa46503014d1a6edb2fb4c5fefb73a7d0d5bc644" resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.2.tgz#3257c0600d197fe7642f3698944b47c91567f379"
dependencies: dependencies:
lodash "^4.17.4" lodash "^4.17.10"
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 "^4.0.0" postcss-selector-parser "^4.0.0"
@@ -6031,9 +6046,9 @@ 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.1: stylelint@^9.3.0:
version "9.2.1" version "9.3.0"
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.2.1.tgz#fe63c169f6cd3bc81e77f0e3c6443df3267ec211" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.3.0.tgz#fe176e4e421ac10eac1a6b6d9f28e908eb58c5db"
dependencies: dependencies:
autoprefixer "^8.0.0" autoprefixer "^8.0.0"
balanced-match "^1.0.0" balanced-match "^1.0.0"
@@ -6058,9 +6073,9 @@ stylelint@^9.2.1:
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.23.6" postcss-html "^0.28.0"
postcss-less "^1.1.5" postcss-less "^2.0.0"
postcss-markdown "^0.23.6" postcss-markdown "^0.28.0"
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"
@@ -6068,7 +6083,7 @@ stylelint@^9.2.1:
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-syntax "^0.28.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"