Utils broken down into seperate files and exports
This commit is contained in:
parent
840e31a693
commit
392dfd024c
3376
dist/plyr.js
vendored
3376
dist/plyr.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/plyr.js.map
vendored
2
dist/plyr.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.min.js
vendored
2
dist/plyr.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.min.js.map
vendored
2
dist/plyr.min.js.map
vendored
File diff suppressed because one or more lines are too long
3374
dist/plyr.polyfilled.js
vendored
3374
dist/plyr.polyfilled.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/plyr.polyfilled.js.map
vendored
2
dist/plyr.polyfilled.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.polyfilled.min.js
vendored
2
dist/plyr.polyfilled.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.polyfilled.min.js.map
vendored
2
dist/plyr.polyfilled.min.js.map
vendored
File diff suppressed because one or more lines are too long
@ -27,7 +27,7 @@
|
|||||||
"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",
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -6,7 +6,13 @@
|
|||||||
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 { createElement, emptyElement, getAttributesFromSelector, insertAfter, removeElement, toggleClass } from './utils/elements';
|
||||||
|
import { on, trigger } 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 +25,7 @@ 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 +33,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,19 +46,18 @@ 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 (url !== null && url.hostname !== window.location.href.hostname && [
|
||||||
'http:',
|
'http:',
|
||||||
'https:',
|
'https:',
|
||||||
].includes(href.protocol)) {
|
].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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -65,14 +67,14 @@ const captions = {
|
|||||||
let active = this.storage.get('captions');
|
let active = this.storage.get('captions');
|
||||||
|
|
||||||
// Otherwise fall back to the default config
|
// Otherwise fall back to the default config
|
||||||
if (!utils.is.boolean(active)) {
|
if (!is.boolean(active)) {
|
||||||
({ active } = this.config.captions);
|
({ active } = this.config.captions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get language from storage, fallback to config
|
// Get language from storage, fallback to config
|
||||||
let language = this.storage.get('language') || this.config.captions.language;
|
let language = this.storage.get('language') || this.config.captions.language;
|
||||||
if (language === 'auto') {
|
if (language === 'auto') {
|
||||||
[ language ] = (navigator.language || navigator.userLanguage).split('-');
|
[language] = (navigator.language || navigator.userLanguage).split('-');
|
||||||
}
|
}
|
||||||
// Set language and show if active
|
// Set language and show if active
|
||||||
captions.setLanguage.call(this, language, active);
|
captions.setLanguage.call(this, language, active);
|
||||||
@ -80,7 +82,7 @@ const captions = {
|
|||||||
// 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(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)
|
||||||
@ -94,9 +96,7 @@ const captions = {
|
|||||||
|
|
||||||
// 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))
|
|
||||||
.forEach(track => {
|
|
||||||
this.debug.log('Track added', track);
|
this.debug.log('Track added', track);
|
||||||
// Attempt to store if the original dom element was "default"
|
// Attempt to store if the original dom element was "default"
|
||||||
meta.set(track, {
|
meta.set(track, {
|
||||||
@ -107,7 +107,7 @@ const captions = {
|
|||||||
track.mode = 'hidden';
|
track.mode = 'hidden';
|
||||||
|
|
||||||
// Add event listener for cue changes
|
// Add event listener for cue changes
|
||||||
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
|
on(track, 'cuechange', () => captions.updateCues.call(this));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ const captions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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')) {
|
||||||
@ -137,7 +137,7 @@ const captions = {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ const captions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
utils.dispatchEvent.call(this, this.media, 'languagechange');
|
trigger.call(this, this.media, 'languagechange');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isHTML5 && this.isVideo) {
|
if (this.isHTML5 && this.isVideo) {
|
||||||
@ -181,7 +181,7 @@ const captions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setLanguage(language, show = true) {
|
setLanguage(language, show = true) {
|
||||||
if (!utils.is.string(language)) {
|
if (!is.string(language)) {
|
||||||
this.debug.warn('Invalid language argument', language);
|
this.debug.warn('Invalid language argument', language);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -202,9 +202,7 @@ const captions = {
|
|||||||
const tracks = Array.from((this.media || {}).textTracks || []);
|
const tracks = Array.from((this.media || {}).textTracks || []);
|
||||||
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
|
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
|
||||||
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
// 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 => [
|
||||||
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
|
|
||||||
.filter(track => [
|
|
||||||
'captions',
|
'captions',
|
||||||
'subtitles',
|
'subtitles',
|
||||||
].includes(track.kind));
|
].includes(track.kind));
|
||||||
@ -222,16 +220,16 @@ const captions = {
|
|||||||
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.active) {
|
||||||
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 +247,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 +265,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 +274,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');
|
trigger.call(this, this.media, 'cuechange');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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 };
|
316
src/js/controls.js
vendored
316
src/js/controls.js
vendored
@ -6,14 +6,17 @@ import captions from './captions';
|
|||||||
import html5 from './html5';
|
import html5 from './html5';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import { repaint, transitionEndEvent } from './utils/animation';
|
||||||
|
import browser from './utils/browser';
|
||||||
// Sniff out the browser
|
import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, removeElement, setAttributes, toggleClass, toggleHidden, toggleState } from './utils/elements';
|
||||||
const browser = utils.getBrowser();
|
import { off, on } from './utils/events';
|
||||||
|
import is from './utils/is';
|
||||||
|
import loadSprite from './utils/loadSprite';
|
||||||
|
import { extend } from './utils/objects';
|
||||||
|
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
|
||||||
|
import { formatTime, getHours } from './utils/time';
|
||||||
|
|
||||||
const controls = {
|
const controls = {
|
||||||
|
|
||||||
|
|
||||||
// Get icon URL
|
// Get icon URL
|
||||||
getIconUrl() {
|
getIconUrl() {
|
||||||
const url = new URL(this.config.iconUrl, window.location);
|
const url = new URL(this.config.iconUrl, window.location);
|
||||||
@ -29,41 +32,41 @@ const controls = {
|
|||||||
// TODO: Allow settings menus with custom controls
|
// TODO: Allow settings menus with custom controls
|
||||||
findElements() {
|
findElements() {
|
||||||
try {
|
try {
|
||||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
this.elements.buttons = {
|
this.elements.buttons = {
|
||||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
play: getElements.call(this, this.config.selectors.buttons.play),
|
||||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
pause: getElement.call(this, this.config.selectors.buttons.pause),
|
||||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
restart: getElement.call(this, this.config.selectors.buttons.restart),
|
||||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
rewind: getElement.call(this, this.config.selectors.buttons.rewind),
|
||||||
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
|
fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),
|
||||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
mute: getElement.call(this, this.config.selectors.buttons.mute),
|
||||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
pip: getElement.call(this, this.config.selectors.buttons.pip),
|
||||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
airplay: getElement.call(this, this.config.selectors.buttons.airplay),
|
||||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
settings: getElement.call(this, this.config.selectors.buttons.settings),
|
||||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
captions: getElement.call(this, this.config.selectors.buttons.captions),
|
||||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Progress
|
// Progress
|
||||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
this.elements.progress = getElement.call(this, this.config.selectors.progress);
|
||||||
|
|
||||||
// Inputs
|
// Inputs
|
||||||
this.elements.inputs = {
|
this.elements.inputs = {
|
||||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
seek: getElement.call(this, this.config.selectors.inputs.seek),
|
||||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
volume: getElement.call(this, this.config.selectors.inputs.volume),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display
|
// Display
|
||||||
this.elements.display = {
|
this.elements.display = {
|
||||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
buffer: getElement.call(this, this.config.selectors.display.buffer),
|
||||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
currentTime: getElement.call(this, this.config.selectors.display.currentTime),
|
||||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
duration: getElement.call(this, this.config.selectors.display.duration),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Seek tooltip
|
// Seek tooltip
|
||||||
if (utils.is.element(this.elements.progress)) {
|
if (is.element(this.elements.progress)) {
|
||||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +90,9 @@ const controls = {
|
|||||||
|
|
||||||
// Create <svg>
|
// Create <svg>
|
||||||
const icon = document.createElementNS(namespace, 'svg');
|
const icon = document.createElementNS(namespace, 'svg');
|
||||||
utils.setAttributes(
|
setAttributes(
|
||||||
icon,
|
icon,
|
||||||
utils.extend(attributes, {
|
extend(attributes, {
|
||||||
role: 'presentation',
|
role: 'presentation',
|
||||||
focusable: 'false',
|
focusable: 'false',
|
||||||
}),
|
}),
|
||||||
@ -138,21 +141,21 @@ const controls = {
|
|||||||
attributes.class = this.config.classNames.hidden;
|
attributes.class = this.config.classNames.hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.createElement('span', attributes, text);
|
return createElement('span', attributes, text);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create a badge
|
// Create a badge
|
||||||
createBadge(text) {
|
createBadge(text) {
|
||||||
if (utils.is.empty(text)) {
|
if (is.empty(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const badge = utils.createElement('span', {
|
const badge = createElement('span', {
|
||||||
class: this.config.classNames.menu.value,
|
class: this.config.classNames.menu.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
badge.appendChild(
|
badge.appendChild(
|
||||||
utils.createElement(
|
createElement(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
class: this.config.classNames.menu.badge,
|
class: this.config.classNames.menu.badge,
|
||||||
@ -166,9 +169,9 @@ const controls = {
|
|||||||
|
|
||||||
// Create a <button>
|
// Create a <button>
|
||||||
createButton(buttonType, attr) {
|
createButton(buttonType, attr) {
|
||||||
const button = utils.createElement('button');
|
const button = createElement('button');
|
||||||
const attributes = Object.assign({}, attr);
|
const attributes = Object.assign({}, attr);
|
||||||
let type = utils.toCamelCase(buttonType);
|
let type = toCamelCase(buttonType);
|
||||||
|
|
||||||
let toggle = false;
|
let toggle = false;
|
||||||
let label;
|
let label;
|
||||||
@ -252,13 +255,13 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Merge attributes
|
// Merge attributes
|
||||||
utils.extend(attributes, utils.getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
||||||
|
|
||||||
utils.setAttributes(button, attributes);
|
setAttributes(button, attributes);
|
||||||
|
|
||||||
// We have multiple play buttons
|
// We have multiple play buttons
|
||||||
if (type === 'play') {
|
if (type === 'play') {
|
||||||
if (!utils.is.array(this.elements.buttons[type])) {
|
if (!is.array(this.elements.buttons[type])) {
|
||||||
this.elements.buttons[type] = [];
|
this.elements.buttons[type] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +276,7 @@ const controls = {
|
|||||||
// Create an <input type='range'>
|
// Create an <input type='range'>
|
||||||
createRange(type, attributes) {
|
createRange(type, attributes) {
|
||||||
// Seek label
|
// Seek label
|
||||||
const label = utils.createElement(
|
const label = createElement(
|
||||||
'label',
|
'label',
|
||||||
{
|
{
|
||||||
for: attributes.id,
|
for: attributes.id,
|
||||||
@ -284,10 +287,10 @@ const controls = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Seek input
|
// Seek input
|
||||||
const input = utils.createElement(
|
const input = createElement(
|
||||||
'input',
|
'input',
|
||||||
utils.extend(
|
extend(
|
||||||
utils.getAttributesFromSelector(this.config.selectors.inputs[type]),
|
getAttributesFromSelector(this.config.selectors.inputs[type]),
|
||||||
{
|
{
|
||||||
type: 'range',
|
type: 'range',
|
||||||
min: 0,
|
min: 0,
|
||||||
@ -319,10 +322,10 @@ const controls = {
|
|||||||
|
|
||||||
// Create a <progress>
|
// Create a <progress>
|
||||||
createProgress(type, attributes) {
|
createProgress(type, attributes) {
|
||||||
const progress = utils.createElement(
|
const progress = createElement(
|
||||||
'progress',
|
'progress',
|
||||||
utils.extend(
|
extend(
|
||||||
utils.getAttributesFromSelector(this.config.selectors.display[type]),
|
getAttributesFromSelector(this.config.selectors.display[type]),
|
||||||
{
|
{
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@ -336,7 +339,7 @@ const controls = {
|
|||||||
|
|
||||||
// Create the label inside
|
// Create the label inside
|
||||||
if (type !== 'volume') {
|
if (type !== 'volume') {
|
||||||
progress.appendChild(utils.createElement('span', null, '0'));
|
progress.appendChild(createElement('span', null, '0'));
|
||||||
|
|
||||||
let suffix = '';
|
let suffix = '';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -362,12 +365,16 @@ const controls = {
|
|||||||
|
|
||||||
// Create time display
|
// Create time display
|
||||||
createTime(type) {
|
createTime(type) {
|
||||||
const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
|
const attributes = getAttributesFromSelector(this.config.selectors.display[type]);
|
||||||
|
|
||||||
const container = utils.createElement('div', utils.extend(attributes, {
|
const container = createElement(
|
||||||
|
'div',
|
||||||
|
extend(attributes, {
|
||||||
class: `plyr__time ${attributes.class}`,
|
class: `plyr__time ${attributes.class}`,
|
||||||
'aria-label': i18n.get(type, this.config),
|
'aria-label': i18n.get(type, this.config),
|
||||||
}), '00:00');
|
}),
|
||||||
|
'00:00',
|
||||||
|
);
|
||||||
|
|
||||||
// Reference for updates
|
// Reference for updates
|
||||||
this.elements.display[type] = container;
|
this.elements.display[type] = container;
|
||||||
@ -376,16 +383,16 @@ const controls = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Create a settings menu item
|
// Create a settings menu item
|
||||||
createMenuItem({value, list, type, title, badge = null, checked = false}) {
|
createMenuItem({ value, list, type, title, badge = null, checked = false }) {
|
||||||
const item = utils.createElement('li');
|
const item = createElement('li');
|
||||||
|
|
||||||
const label = utils.createElement('label', {
|
const label = createElement('label', {
|
||||||
class: this.config.classNames.control,
|
class: this.config.classNames.control,
|
||||||
});
|
});
|
||||||
|
|
||||||
const radio = utils.createElement(
|
const radio = createElement(
|
||||||
'input',
|
'input',
|
||||||
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs[type]), {
|
extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
name: `plyr-${type}`,
|
name: `plyr-${type}`,
|
||||||
value,
|
value,
|
||||||
@ -394,13 +401,13 @@ const controls = {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const faux = utils.createElement('span', { hidden: '' });
|
const faux = createElement('span', { hidden: '' });
|
||||||
|
|
||||||
label.appendChild(radio);
|
label.appendChild(radio);
|
||||||
label.appendChild(faux);
|
label.appendChild(faux);
|
||||||
label.insertAdjacentHTML('beforeend', title);
|
label.insertAdjacentHTML('beforeend', title);
|
||||||
|
|
||||||
if (utils.is.element(badge)) {
|
if (is.element(badge)) {
|
||||||
label.appendChild(badge);
|
label.appendChild(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,15 +418,15 @@ const controls = {
|
|||||||
// Update the displayed time
|
// Update the displayed time
|
||||||
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
||||||
// Bail if there's no element to display or the value isn't a number
|
// Bail if there's no element to display or the value isn't a number
|
||||||
if (!utils.is.element(target) || !utils.is.number(time)) {
|
if (!is.element(target) || !is.number(time)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always display hours if duration is over an hour
|
// Always display hours if duration is over an hour
|
||||||
const forceHours = utils.getHours(this.duration) > 0;
|
const forceHours = getHours(this.duration) > 0;
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
target.innerText = utils.formatTime(time, forceHours, inverted);
|
target.innerText = formatTime(time, forceHours, inverted);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update volume UI and storage
|
// Update volume UI and storage
|
||||||
@ -429,19 +436,19 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update range
|
// Update range
|
||||||
if (utils.is.element(this.elements.inputs.volume)) {
|
if (is.element(this.elements.inputs.volume)) {
|
||||||
controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update mute state
|
// Update mute state
|
||||||
if (utils.is.element(this.elements.buttons.mute)) {
|
if (is.element(this.elements.buttons.mute)) {
|
||||||
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update seek value and lower fill
|
// Update seek value and lower fill
|
||||||
setRange(target, value = 0) {
|
setRange(target, value = 0) {
|
||||||
if (!utils.is.element(target)) {
|
if (!is.element(target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,23 +461,23 @@ const controls = {
|
|||||||
|
|
||||||
// Update <progress> elements
|
// Update <progress> elements
|
||||||
updateProgress(event) {
|
updateProgress(event) {
|
||||||
if (!this.supported.ui || !utils.is.event(event)) {
|
if (!this.supported.ui || !is.event(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = 0;
|
let value = 0;
|
||||||
|
|
||||||
const setProgress = (target, input) => {
|
const setProgress = (target, input) => {
|
||||||
const value = utils.is.number(input) ? input : 0;
|
const value = is.number(input) ? input : 0;
|
||||||
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
|
const progress = is.element(target) ? target : this.elements.display.buffer;
|
||||||
|
|
||||||
// Update value and label
|
// Update value and label
|
||||||
if (utils.is.element(progress)) {
|
if (is.element(progress)) {
|
||||||
progress.value = value;
|
progress.value = value;
|
||||||
|
|
||||||
// Update text label inside
|
// Update text label inside
|
||||||
const label = progress.getElementsByTagName('span')[0];
|
const label = progress.getElementsByTagName('span')[0];
|
||||||
if (utils.is.element(label)) {
|
if (is.element(label)) {
|
||||||
label.childNodes[0].nodeValue = value;
|
label.childNodes[0].nodeValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -482,7 +489,7 @@ const controls = {
|
|||||||
case 'timeupdate':
|
case 'timeupdate':
|
||||||
case 'seeking':
|
case 'seeking':
|
||||||
case 'seeked':
|
case 'seeked':
|
||||||
value = utils.getPercentage(this.currentTime, this.duration);
|
value = getPercentage(this.currentTime, this.duration);
|
||||||
|
|
||||||
// Set seek range value only if it's a 'natural' time event
|
// Set seek range value only if it's a 'natural' time event
|
||||||
if (event.type === 'timeupdate') {
|
if (event.type === 'timeupdate') {
|
||||||
@ -507,10 +514,10 @@ const controls = {
|
|||||||
// Webkit polyfill for lower fill range
|
// Webkit polyfill for lower fill range
|
||||||
updateRangeFill(target) {
|
updateRangeFill(target) {
|
||||||
// Get range from event if event passed
|
// Get range from event if event passed
|
||||||
const range = utils.is.event(target) ? target.target : target;
|
const range = is.event(target) ? target.target : target;
|
||||||
|
|
||||||
// Needs to be a valid <input type='range'>
|
// Needs to be a valid <input type='range'>
|
||||||
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
|
if (!is.element(range) || range.getAttribute('type') !== 'range') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,12 +536,7 @@ const controls = {
|
|||||||
// Update hover tooltip for seeking
|
// Update hover tooltip for seeking
|
||||||
updateSeekTooltip(event) {
|
updateSeekTooltip(event) {
|
||||||
// Bail if setting not true
|
// Bail if setting not true
|
||||||
if (
|
if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {
|
||||||
!this.config.tooltips.seek ||
|
|
||||||
!utils.is.element(this.elements.inputs.seek) ||
|
|
||||||
!utils.is.element(this.elements.display.seekTooltip) ||
|
|
||||||
this.duration === 0
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,7 +546,7 @@ const controls = {
|
|||||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||||
|
|
||||||
const toggle = toggle => {
|
const toggle = toggle => {
|
||||||
utils.toggleClass(this.elements.display.seekTooltip, visible, toggle);
|
toggleClass(this.elements.display.seekTooltip, visible, toggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hide on touch
|
// Hide on touch
|
||||||
@ -554,9 +556,9 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine percentage, if already visible
|
// Determine percentage, if already visible
|
||||||
if (utils.is.event(event)) {
|
if (is.event(event)) {
|
||||||
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||||
} else if (utils.hasClass(this.elements.display.seekTooltip, visible)) {
|
} else if (hasClass(this.elements.display.seekTooltip, visible)) {
|
||||||
percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
|
percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@ -577,7 +579,7 @@ const controls = {
|
|||||||
|
|
||||||
// Show/hide the tooltip
|
// Show/hide the tooltip
|
||||||
// If the event is a moues in/out and percentage is inside bounds
|
// If the event is a moues in/out and percentage is inside bounds
|
||||||
if (utils.is.event(event) && [
|
if (is.event(event) && [
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
].includes(event.type)) {
|
].includes(event.type)) {
|
||||||
@ -588,7 +590,7 @@ const controls = {
|
|||||||
// Handle time change event
|
// Handle time change event
|
||||||
timeUpdate(event) {
|
timeUpdate(event) {
|
||||||
// Only invert if only one time element is displayed and used for both duration and currentTime
|
// Only invert if only one time element is displayed and used for both duration and currentTime
|
||||||
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
|
const invert = !is.element(this.elements.display.duration) && this.config.invertTime;
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
||||||
@ -610,7 +612,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If there's a spot to display duration
|
// If there's a spot to display duration
|
||||||
const hasDuration = utils.is.element(this.elements.display.duration);
|
const hasDuration = is.element(this.elements.display.duration);
|
||||||
|
|
||||||
// If there's only one time display, display duration there
|
// If there's only one time display, display duration there
|
||||||
if (!hasDuration && this.config.displayDuration && this.paused) {
|
if (!hasDuration && this.config.displayDuration && this.paused) {
|
||||||
@ -628,14 +630,14 @@ const controls = {
|
|||||||
|
|
||||||
// Hide/show a tab
|
// Hide/show a tab
|
||||||
toggleTab(setting, toggle) {
|
toggleTab(setting, toggle) {
|
||||||
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the quality menu
|
// Set the quality menu
|
||||||
// TODO: Vimeo support
|
// TODO: Vimeo support
|
||||||
setQualityMenu(options) {
|
setQualityMenu(options) {
|
||||||
// Menu required
|
// Menu required
|
||||||
if (!utils.is.element(this.elements.settings.panes.quality)) {
|
if (!is.element(this.elements.settings.panes.quality)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,12 +645,12 @@ const controls = {
|
|||||||
const list = this.elements.settings.panes.quality.querySelector('ul');
|
const list = this.elements.settings.panes.quality.querySelector('ul');
|
||||||
|
|
||||||
// Set options if passed and filter based on config
|
// Set options if passed and filter based on config
|
||||||
if (utils.is.array(options)) {
|
if (is.array(options)) {
|
||||||
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
|
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
|
const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
|
||||||
controls.toggleTab.call(this, type, toggle);
|
controls.toggleTab.call(this, type, toggle);
|
||||||
|
|
||||||
// Check if we need to toggle the parent
|
// Check if we need to toggle the parent
|
||||||
@ -660,7 +662,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Empty the menu
|
// Empty the menu
|
||||||
utils.emptyElement(list);
|
emptyElement(list);
|
||||||
|
|
||||||
// Get the badge HTML for HD, 4K etc
|
// Get the badge HTML for HD, 4K etc
|
||||||
const getBadge = quality => {
|
const getBadge = quality => {
|
||||||
@ -699,7 +701,7 @@ const controls = {
|
|||||||
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
||||||
|
|
||||||
case 'quality':
|
case 'quality':
|
||||||
if (utils.is.number(value)) {
|
if (is.number(value)) {
|
||||||
const label = i18n.get(`qualityLabel.${value}`, this.config);
|
const label = i18n.get(`qualityLabel.${value}`, this.config);
|
||||||
|
|
||||||
if (!label.length) {
|
if (!label.length) {
|
||||||
@ -709,7 +711,7 @@ const controls = {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.toTitleCase(value);
|
return toTitleCase(value);
|
||||||
|
|
||||||
case 'captions':
|
case 'captions':
|
||||||
return captions.getLabel.call(this);
|
return captions.getLabel.call(this);
|
||||||
@ -731,15 +733,15 @@ const controls = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
value = !utils.is.empty(input) ? input : this[setting];
|
value = !is.empty(input) ? input : this[setting];
|
||||||
|
|
||||||
// Get default
|
// Get default
|
||||||
if (utils.is.empty(value)) {
|
if (is.empty(value)) {
|
||||||
value = this.config[setting].default;
|
value = this.config[setting].default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported value
|
// Unsupported value
|
||||||
if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
|
if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
|
||||||
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
|
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -754,12 +756,12 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the list if we need to
|
// Get the list if we need to
|
||||||
if (!utils.is.element(list)) {
|
if (!is.element(list)) {
|
||||||
list = pane && pane.querySelector('ul');
|
list = pane && pane.querySelector('ul');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's no list it means it's not been rendered...
|
// If there's no list it means it's not been rendered...
|
||||||
if (!utils.is.element(list)) {
|
if (!is.element(list)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -770,7 +772,7 @@ const controls = {
|
|||||||
// Find the radio option and check it
|
// Find the radio option and check it
|
||||||
const target = list && list.querySelector(`input[value="${value}"]`);
|
const target = list && list.querySelector(`input[value="${value}"]`);
|
||||||
|
|
||||||
if (utils.is.element(target)) {
|
if (is.element(target)) {
|
||||||
target.checked = true;
|
target.checked = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -778,7 +780,7 @@ const controls = {
|
|||||||
// Set the looping options
|
// Set the looping options
|
||||||
/* setLoopMenu() {
|
/* setLoopMenu() {
|
||||||
// Menu required
|
// Menu required
|
||||||
if (!utils.is.element(this.elements.settings.panes.loop)) {
|
if (!is.element(this.elements.settings.panes.loop)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -786,22 +788,22 @@ const controls = {
|
|||||||
const list = this.elements.settings.panes.loop.querySelector('ul');
|
const list = this.elements.settings.panes.loop.querySelector('ul');
|
||||||
|
|
||||||
// Show the pane and tab
|
// Show the pane and tab
|
||||||
utils.toggleHidden(this.elements.settings.tabs.loop, false);
|
toggleHidden(this.elements.settings.tabs.loop, false);
|
||||||
utils.toggleHidden(this.elements.settings.panes.loop, false);
|
toggleHidden(this.elements.settings.panes.loop, false);
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const toggle = !utils.is.empty(this.loop.options);
|
const toggle = !is.empty(this.loop.options);
|
||||||
controls.toggleTab.call(this, 'loop', toggle);
|
controls.toggleTab.call(this, 'loop', toggle);
|
||||||
|
|
||||||
// Empty the menu
|
// Empty the menu
|
||||||
utils.emptyElement(list);
|
emptyElement(list);
|
||||||
|
|
||||||
options.forEach(option => {
|
options.forEach(option => {
|
||||||
const item = utils.createElement('li');
|
const item = createElement('li');
|
||||||
|
|
||||||
const button = utils.createElement(
|
const button = createElement(
|
||||||
'button',
|
'button',
|
||||||
utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.loop), {
|
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: this.config.classNames.control,
|
class: this.config.classNames.control,
|
||||||
'data-plyr-loop-action': option,
|
'data-plyr-loop-action': option,
|
||||||
@ -833,7 +835,7 @@ const controls = {
|
|||||||
controls.toggleTab.call(this, type, tracks.length);
|
controls.toggleTab.call(this, type, tracks.length);
|
||||||
|
|
||||||
// Empty the menu
|
// Empty the menu
|
||||||
utils.emptyElement(list);
|
emptyElement(list);
|
||||||
|
|
||||||
// Check if we need to toggle the parent
|
// Check if we need to toggle the parent
|
||||||
controls.checkMenu.call(this);
|
controls.checkMenu.call(this);
|
||||||
@ -876,14 +878,14 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Menu required
|
// Menu required
|
||||||
if (!utils.is.element(this.elements.settings.panes.speed)) {
|
if (!is.element(this.elements.settings.panes.speed)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = 'speed';
|
const type = 'speed';
|
||||||
|
|
||||||
// Set the speed options
|
// Set the speed options
|
||||||
if (utils.is.array(options)) {
|
if (is.array(options)) {
|
||||||
this.options.speed = options;
|
this.options.speed = options;
|
||||||
} else if (this.isHTML5 || this.isVimeo) {
|
} else if (this.isHTML5 || this.isVimeo) {
|
||||||
this.options.speed = [
|
this.options.speed = [
|
||||||
@ -901,7 +903,7 @@ const controls = {
|
|||||||
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
|
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const toggle = !utils.is.empty(this.options.speed) && this.options.speed.length > 1;
|
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
|
||||||
controls.toggleTab.call(this, type, toggle);
|
controls.toggleTab.call(this, type, toggle);
|
||||||
|
|
||||||
// Check if we need to toggle the parent
|
// Check if we need to toggle the parent
|
||||||
@ -916,7 +918,7 @@ const controls = {
|
|||||||
const list = this.elements.settings.panes.speed.querySelector('ul');
|
const list = this.elements.settings.panes.speed.querySelector('ul');
|
||||||
|
|
||||||
// Empty the menu
|
// Empty the menu
|
||||||
utils.emptyElement(list);
|
emptyElement(list);
|
||||||
|
|
||||||
// Create items
|
// Create items
|
||||||
this.options.speed.forEach(speed => {
|
this.options.speed.forEach(speed => {
|
||||||
@ -934,9 +936,9 @@ const controls = {
|
|||||||
// Check if we need to hide/show the settings menu
|
// Check if we need to hide/show the settings menu
|
||||||
checkMenu() {
|
checkMenu() {
|
||||||
const { tabs } = this.elements.settings;
|
const { tabs } = this.elements.settings;
|
||||||
const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
|
const visible = !is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
|
||||||
|
|
||||||
utils.toggleHidden(this.elements.settings.menu, !visible);
|
toggleHidden(this.elements.settings.menu, !visible);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Show/hide menu
|
// Show/hide menu
|
||||||
@ -945,14 +947,14 @@ const controls = {
|
|||||||
const button = this.elements.buttons.settings;
|
const button = this.elements.buttons.settings;
|
||||||
|
|
||||||
// Menu and button are required
|
// Menu and button are required
|
||||||
if (!utils.is.element(form) || !utils.is.element(button)) {
|
if (!is.element(form) || !is.element(button)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.hasAttribute('hidden');
|
const show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden');
|
||||||
|
|
||||||
if (utils.is.event(event)) {
|
if (is.event(event)) {
|
||||||
const isMenuItem = utils.is.element(form) && form.contains(event.target);
|
const isMenuItem = is.element(form) && form.contains(event.target);
|
||||||
const isButton = event.target === this.elements.buttons.settings;
|
const isButton = event.target === this.elements.buttons.settings;
|
||||||
|
|
||||||
// If the click was inside the form or if the click
|
// If the click was inside the form or if the click
|
||||||
@ -969,13 +971,13 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set form and button attributes
|
// Set form and button attributes
|
||||||
if (utils.is.element(button)) {
|
if (is.element(button)) {
|
||||||
button.setAttribute('aria-expanded', show);
|
button.setAttribute('aria-expanded', show);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.element(form)) {
|
if (is.element(form)) {
|
||||||
utils.toggleHidden(form, !show);
|
toggleHidden(form, !show);
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
|
toggleClass(this.elements.container, this.config.classNames.menu.open, show);
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
form.removeAttribute('tabindex');
|
form.removeAttribute('tabindex');
|
||||||
@ -1006,7 +1008,7 @@ const controls = {
|
|||||||
const height = clone.scrollHeight;
|
const height = clone.scrollHeight;
|
||||||
|
|
||||||
// Remove from the DOM
|
// Remove from the DOM
|
||||||
utils.removeElement(clone);
|
removeElement(clone);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
@ -1020,7 +1022,7 @@ const controls = {
|
|||||||
const pane = document.getElementById(target);
|
const pane = document.getElementById(target);
|
||||||
|
|
||||||
// Nothing to show, bail
|
// Nothing to show, bail
|
||||||
if (!utils.is.element(pane)) {
|
if (!is.element(pane)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1064,11 +1066,11 @@ const controls = {
|
|||||||
container.style.height = '';
|
container.style.height = '';
|
||||||
|
|
||||||
// Only listen once
|
// Only listen once
|
||||||
utils.off(container, utils.transitionEndEvent, restore);
|
off(container, transitionEndEvent, restore);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen for the transition finishing and restore auto height/width
|
// Listen for the transition finishing and restore auto height/width
|
||||||
utils.on(container, utils.transitionEndEvent, restore);
|
on(container, transitionEndEvent, restore);
|
||||||
|
|
||||||
// Set dimensions to target
|
// Set dimensions to target
|
||||||
container.style.width = `${size.width}px`;
|
container.style.width = `${size.width}px`;
|
||||||
@ -1076,13 +1078,13 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set attributes on current tab
|
// Set attributes on current tab
|
||||||
utils.toggleHidden(current, true);
|
toggleHidden(current, true);
|
||||||
current.setAttribute('tabindex', -1);
|
current.setAttribute('tabindex', -1);
|
||||||
|
|
||||||
// Set attributes on target
|
// Set attributes on target
|
||||||
utils.toggleHidden(pane, false);
|
toggleHidden(pane, false);
|
||||||
|
|
||||||
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
|
const tabs = getElements.call(this, `[aria-controls="${target}"]`);
|
||||||
Array.from(tabs).forEach(tab => {
|
Array.from(tabs).forEach(tab => {
|
||||||
tab.setAttribute('aria-expanded', true);
|
tab.setAttribute('aria-expanded', true);
|
||||||
});
|
});
|
||||||
@ -1096,12 +1098,12 @@ const controls = {
|
|||||||
// TODO: Set order based on order in the config.controls array?
|
// TODO: Set order based on order in the config.controls array?
|
||||||
create(data) {
|
create(data) {
|
||||||
// Do nothing if we want no controls
|
// Do nothing if we want no controls
|
||||||
if (utils.is.empty(this.config.controls)) {
|
if (is.empty(this.config.controls)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the container
|
// Create the container
|
||||||
const container = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.controls.wrapper));
|
const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
|
||||||
|
|
||||||
// Restart button
|
// Restart button
|
||||||
if (this.config.controls.includes('restart')) {
|
if (this.config.controls.includes('restart')) {
|
||||||
@ -1125,7 +1127,7 @@ const controls = {
|
|||||||
|
|
||||||
// Progress
|
// Progress
|
||||||
if (this.config.controls.includes('progress')) {
|
if (this.config.controls.includes('progress')) {
|
||||||
const progress = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.progress));
|
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
|
||||||
|
|
||||||
// Seek range slider
|
// Seek range slider
|
||||||
const seek = controls.createRange.call(this, 'seek', {
|
const seek = controls.createRange.call(this, 'seek', {
|
||||||
@ -1141,7 +1143,7 @@ const controls = {
|
|||||||
|
|
||||||
// Seek tooltip
|
// Seek tooltip
|
||||||
if (this.config.tooltips.seek) {
|
if (this.config.tooltips.seek) {
|
||||||
const tooltip = utils.createElement(
|
const tooltip = createElement(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
class: this.config.classNames.tooltip,
|
class: this.config.classNames.tooltip,
|
||||||
@ -1174,7 +1176,7 @@ const controls = {
|
|||||||
|
|
||||||
// Volume range control
|
// Volume range control
|
||||||
if (this.config.controls.includes('volume')) {
|
if (this.config.controls.includes('volume')) {
|
||||||
const volume = utils.createElement('div', {
|
const volume = createElement('div', {
|
||||||
class: 'plyr__volume',
|
class: 'plyr__volume',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1189,7 +1191,7 @@ const controls = {
|
|||||||
const range = controls.createRange.call(
|
const range = controls.createRange.call(
|
||||||
this,
|
this,
|
||||||
'volume',
|
'volume',
|
||||||
utils.extend(attributes, {
|
extend(attributes, {
|
||||||
id: `plyr-volume-${data.id}`,
|
id: `plyr-volume-${data.id}`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -1207,8 +1209,8 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings button / menu
|
// Settings button / menu
|
||||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
|
||||||
const menu = utils.createElement('div', {
|
const menu = createElement('div', {
|
||||||
class: 'plyr__menu',
|
class: 'plyr__menu',
|
||||||
hidden: '',
|
hidden: '',
|
||||||
});
|
});
|
||||||
@ -1222,7 +1224,7 @@ const controls = {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = utils.createElement('form', {
|
const form = createElement('form', {
|
||||||
class: 'plyr__menu__container',
|
class: 'plyr__menu__container',
|
||||||
id: `plyr-settings-${data.id}`,
|
id: `plyr-settings-${data.id}`,
|
||||||
hidden: '',
|
hidden: '',
|
||||||
@ -1231,29 +1233,29 @@ const controls = {
|
|||||||
tabindex: -1,
|
tabindex: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inner = utils.createElement('div');
|
const inner = createElement('div');
|
||||||
|
|
||||||
const home = utils.createElement('div', {
|
const home = createElement('div', {
|
||||||
id: `plyr-settings-${data.id}-home`,
|
id: `plyr-settings-${data.id}-home`,
|
||||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||||
role: 'tabpanel',
|
role: 'tabpanel',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the tab list
|
// Create the tab list
|
||||||
const tabs = utils.createElement('ul', {
|
const tabs = createElement('ul', {
|
||||||
role: 'tablist',
|
role: 'tablist',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build the tabs
|
// Build the tabs
|
||||||
this.config.settings.forEach(type => {
|
this.config.settings.forEach(type => {
|
||||||
const tab = utils.createElement('li', {
|
const tab = createElement('li', {
|
||||||
role: 'tab',
|
role: 'tab',
|
||||||
hidden: '',
|
hidden: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const button = utils.createElement(
|
const button = createElement(
|
||||||
'button',
|
'button',
|
||||||
utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||||
id: `plyr-settings-${data.id}-${type}-tab`,
|
id: `plyr-settings-${data.id}-${type}-tab`,
|
||||||
@ -1264,7 +1266,7 @@ const controls = {
|
|||||||
i18n.get(type, this.config),
|
i18n.get(type, this.config),
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = utils.createElement('span', {
|
const value = createElement('span', {
|
||||||
class: this.config.classNames.menu.value,
|
class: this.config.classNames.menu.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1283,7 +1285,7 @@ const controls = {
|
|||||||
|
|
||||||
// Build the panes
|
// Build the panes
|
||||||
this.config.settings.forEach(type => {
|
this.config.settings.forEach(type => {
|
||||||
const pane = utils.createElement('div', {
|
const pane = createElement('div', {
|
||||||
id: `plyr-settings-${data.id}-${type}`,
|
id: `plyr-settings-${data.id}-${type}`,
|
||||||
hidden: '',
|
hidden: '',
|
||||||
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
|
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
|
||||||
@ -1291,7 +1293,7 @@ const controls = {
|
|||||||
tabindex: -1,
|
tabindex: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const back = utils.createElement(
|
const back = createElement(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@ -1305,7 +1307,7 @@ const controls = {
|
|||||||
|
|
||||||
pane.appendChild(back);
|
pane.appendChild(back);
|
||||||
|
|
||||||
const options = utils.createElement('ul');
|
const options = createElement('ul');
|
||||||
|
|
||||||
pane.appendChild(options);
|
pane.appendChild(options);
|
||||||
inner.appendChild(pane);
|
inner.appendChild(pane);
|
||||||
@ -1360,7 +1362,7 @@ const controls = {
|
|||||||
|
|
||||||
// Only load external sprite using AJAX
|
// Only load external sprite using AJAX
|
||||||
if (icon.cors) {
|
if (icon.cors) {
|
||||||
utils.loadSprite(icon.url, 'sprite-plyr');
|
loadSprite(icon.url, 'sprite-plyr');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1379,10 +1381,10 @@ const controls = {
|
|||||||
};
|
};
|
||||||
let update = true;
|
let update = true;
|
||||||
|
|
||||||
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
|
if (is.string(this.config.controls) || is.element(this.config.controls)) {
|
||||||
// String or HTMLElement passed as the option
|
// String or HTMLElement passed as the option
|
||||||
container = this.config.controls;
|
container = this.config.controls;
|
||||||
} else if (utils.is.function(this.config.controls)) {
|
} else if (is.function(this.config.controls)) {
|
||||||
// A custom function to build controls
|
// A custom function to build controls
|
||||||
// The function can return a HTMLElement or String
|
// The function can return a HTMLElement or String
|
||||||
container = this.config.controls.call(this, props);
|
container = this.config.controls.call(this, props);
|
||||||
@ -1408,7 +1410,7 @@ const controls = {
|
|||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
]) => {
|
]) => {
|
||||||
result = utils.replaceAll(result, `{${key}}`, value);
|
result = replaceAll(result, `{${key}}`, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -1416,9 +1418,9 @@ const controls = {
|
|||||||
|
|
||||||
// Update markup
|
// Update markup
|
||||||
if (update) {
|
if (update) {
|
||||||
if (utils.is.string(this.config.controls)) {
|
if (is.string(this.config.controls)) {
|
||||||
container = replace(container);
|
container = replace(container);
|
||||||
} else if (utils.is.element(container)) {
|
} else if (is.element(container)) {
|
||||||
container.innerHTML = replace(container.innerHTML);
|
container.innerHTML = replace(container.innerHTML);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1427,35 +1429,35 @@ const controls = {
|
|||||||
let target;
|
let target;
|
||||||
|
|
||||||
// Inject to custom location
|
// Inject to custom location
|
||||||
if (utils.is.string(this.config.selectors.controls.container)) {
|
if (is.string(this.config.selectors.controls.container)) {
|
||||||
target = document.querySelector(this.config.selectors.controls.container);
|
target = document.querySelector(this.config.selectors.controls.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject into the container by default
|
// Inject into the container by default
|
||||||
if (!utils.is.element(target)) {
|
if (!is.element(target)) {
|
||||||
target = this.elements.container;
|
target = this.elements.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject controls HTML
|
// Inject controls HTML
|
||||||
if (utils.is.element(container)) {
|
if (is.element(container)) {
|
||||||
target.appendChild(container);
|
target.appendChild(container);
|
||||||
} else if (container) {
|
} else if (container) {
|
||||||
target.insertAdjacentHTML('beforeend', container);
|
target.insertAdjacentHTML('beforeend', container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the elements if need be
|
// Find the elements if need be
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
if (!is.element(this.elements.controls)) {
|
||||||
controls.findElements.call(this);
|
controls.findElements.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edge sometimes doesn't finish the paint so force a redraw
|
// Edge sometimes doesn't finish the paint so force a redraw
|
||||||
if (window.navigator.userAgent.includes('Edge')) {
|
if (window.navigator.userAgent.includes('Edge')) {
|
||||||
utils.repaint(target);
|
repaint(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup tooltips
|
// Setup tooltips
|
||||||
if (this.config.tooltips.controls) {
|
if (this.config.tooltips.controls) {
|
||||||
const labels = utils.getElements.call(
|
const labels = getElements.call(
|
||||||
this,
|
this,
|
||||||
[
|
[
|
||||||
this.config.selectors.controls.wrapper,
|
this.config.selectors.controls.wrapper,
|
||||||
@ -1467,8 +1469,8 @@ const controls = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Array.from(labels).forEach(label => {
|
Array.from(labels).forEach(label => {
|
||||||
utils.toggleClass(label, this.config.classNames.hidden, false);
|
toggleClass(label, this.config.classNames.hidden, false);
|
||||||
utils.toggleClass(label, this.config.classNames.tooltip, true);
|
toggleClass(label, this.config.classNames.tooltip, true);
|
||||||
label.setAttribute('role', 'tooltip');
|
label.setAttribute('role', 'tooltip');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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, toggleState, trapFocus } from './utils/elements';
|
||||||
const browser = utils.getBrowser();
|
import { on, trigger } 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)) {
|
||||||
utils.toggleState(button, this.active);
|
toggleState(button, this.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an event
|
// Trigger an event
|
||||||
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
trigger.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,15 @@ 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(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(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ class Fullscreen {
|
|||||||
// 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 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +103,7 @@ class Fullscreen {
|
|||||||
];
|
];
|
||||||
|
|
||||||
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 +136,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`];
|
||||||
@ -157,7 +158,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 +176,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 +195,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}`]();
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import { dedupe } from './utils/arrays';
|
||||||
|
import { removeElement } from './utils/elements';
|
||||||
|
import { trigger } from './utils/events';
|
||||||
|
import is from './utils/is';
|
||||||
|
|
||||||
const html5 = {
|
const html5 = {
|
||||||
getSources() {
|
getSources() {
|
||||||
@ -23,20 +26,20 @@ const html5 = {
|
|||||||
// Get sources
|
// Get sources
|
||||||
const sources = html5.getSources.call(this);
|
const sources = html5.getSources.call(this);
|
||||||
|
|
||||||
if (utils.is.empty(sources)) {
|
if (is.empty(sources)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get <source> with size attribute
|
// Get <source> with size attribute
|
||||||
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
|
const sizes = Array.from(sources).filter(source => !is.empty(source.getAttribute('size')));
|
||||||
|
|
||||||
// If none, bail
|
// If none, bail
|
||||||
if (utils.is.empty(sizes)) {
|
if (is.empty(sizes)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce to unique list
|
// Reduce to unique list
|
||||||
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
|
return dedupe(sizes.map(source => Number(source.getAttribute('size'))));
|
||||||
},
|
},
|
||||||
|
|
||||||
extend() {
|
extend() {
|
||||||
@ -52,13 +55,13 @@ const html5 = {
|
|||||||
// Get sources
|
// Get sources
|
||||||
const sources = html5.getSources.call(player);
|
const sources = html5.getSources.call(player);
|
||||||
|
|
||||||
if (utils.is.empty(sources)) {
|
if (is.empty(sources)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
|
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
|
||||||
|
|
||||||
if (utils.is.empty(matches)) {
|
if (is.empty(matches)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +71,7 @@ const html5 = {
|
|||||||
// Get sources
|
// Get sources
|
||||||
const sources = html5.getSources.call(player);
|
const sources = html5.getSources.call(player);
|
||||||
|
|
||||||
if (utils.is.empty(sources)) {
|
if (is.empty(sources)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +79,7 @@ const html5 = {
|
|||||||
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
|
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
|
||||||
|
|
||||||
// No matches for requested size
|
// No matches for requested size
|
||||||
if (utils.is.empty(matches)) {
|
if (is.empty(matches)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,12 +87,12 @@ const html5 = {
|
|||||||
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
|
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
|
||||||
|
|
||||||
// No supported sources
|
// No supported sources
|
||||||
if (utils.is.empty(supported)) {
|
if (is.empty(supported)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger change event
|
// Trigger change event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
trigger.call(player, player.media, 'qualityrequested', false, {
|
||||||
quality: input,
|
quality: input,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -115,7 +118,7 @@ const html5 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger change event
|
// Trigger change event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
trigger.call(player, player.media, 'qualitychange', false, {
|
||||||
quality: input,
|
quality: input,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -130,7 +133,7 @@ const html5 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove child sources
|
// Remove child sources
|
||||||
utils.removeElement(html5.getSources());
|
removeElement(html5.getSources());
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -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 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ const i18n = {
|
|||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
]) => {
|
]) => {
|
||||||
string = utils.replaceAll(string, key, value);
|
string = replaceAll(string, key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
return string;
|
return string;
|
||||||
|
@ -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 { off, on, toggleListener, trigger } 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,10 +73,10 @@ class Listeners {
|
|||||||
// 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 (is.element(focused) && (
|
||||||
focused !== this.player.elements.inputs.seek &&
|
focused !== this.player.elements.inputs.seek &&
|
||||||
utils.matches(focused, this.player.config.selectors.editable))
|
matches(focused, this.player.config.selectors.editable))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -195,41 +195,41 @@ 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
|
// Clean up
|
||||||
utils.off(document.body, 'touchstart', this.firstTouch);
|
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(window, 'keydown keyup', this.handleKey, toggle, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click anywhere closes menu
|
// Click anywhere closes menu
|
||||||
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
|
toggleListener(document.body, 'click', this.toggleMenu, toggle);
|
||||||
|
|
||||||
// Detect touch by events
|
// Detect touch by events
|
||||||
utils.on(document.body, 'touchstart', this.firstTouch);
|
on(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(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(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(this.player.elements.container, 'keydown', event => {
|
||||||
if (event.keyCode !== 9) {
|
if (event.keyCode !== 9) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -237,12 +237,12 @@ 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(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
||||||
const { controls } = this.player.elements;
|
const { controls } = this.player.elements;
|
||||||
|
|
||||||
// Remove button states for fullscreen
|
// Remove button states for fullscreen
|
||||||
@ -276,20 +276,20 @@ class Listeners {
|
|||||||
// 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(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(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(this.player.media, 'loadeddata 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(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 +298,20 @@ 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(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(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(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(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(this.player.media, 'playing', () => {
|
||||||
if (!this.player.ads) {
|
if (!this.player.ads) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -326,15 +326,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(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 +353,7 @@ 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(
|
||||||
this.player.elements.wrapper,
|
this.player.elements.wrapper,
|
||||||
'contextmenu',
|
'contextmenu',
|
||||||
event => {
|
event => {
|
||||||
@ -364,13 +364,13 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume change
|
// Volume change
|
||||||
utils.on(this.player.media, 'volumechange', () => {
|
on(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(this.player.media, 'ratechange', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this.player, 'speed');
|
controls.updateSetting.call(this.player, 'speed');
|
||||||
|
|
||||||
@ -379,19 +379,19 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Quality request
|
// Quality request
|
||||||
utils.on(this.player.media, 'qualityrequested', event => {
|
on(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(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
|
// Caption language change
|
||||||
utils.on(this.player.media, 'languagechange', () => {
|
on(this.player.media, 'languagechange', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this.player, 'captions');
|
controls.updateSetting.call(this.player, 'captions');
|
||||||
|
|
||||||
@ -400,7 +400,7 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Captions toggle
|
// Captions toggle
|
||||||
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
|
on(this.player.media, 'captionsenabled captionsdisabled', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this.player, 'captions');
|
controls.updateSetting.call(this.player, 'captions');
|
||||||
|
|
||||||
@ -410,7 +410,7 @@ class Listeners {
|
|||||||
|
|
||||||
// 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([
|
on(this.player.media, this.player.config.events.concat([
|
||||||
'keyup',
|
'keyup',
|
||||||
'keydown',
|
'keydown',
|
||||||
]).join(' '), event => {
|
]).join(' '), event => {
|
||||||
@ -421,7 +421,7 @@ class Listeners {
|
|||||||
detail = this.player.media.error;
|
detail = this.player.media.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail);
|
trigger.call(this.player, this.player.elements.container, event.type, true, detail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,7 +433,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 +442,33 @@ 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(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');
|
bind(this.player.elements.buttons.play, '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 +478,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 +491,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 +501,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 +519,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 +528,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 +537,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 +553,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;
|
||||||
@ -590,7 +590,7 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Seek
|
// Seek
|
||||||
on(
|
bind(
|
||||||
this.player.elements.inputs.seek,
|
this.player.elements.inputs.seek,
|
||||||
inputEvent,
|
inputEvent,
|
||||||
event => {
|
event => {
|
||||||
@ -599,7 +599,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 +612,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 +626,7 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume
|
// Volume
|
||||||
on(
|
bind(
|
||||||
this.player.elements.inputs.volume,
|
this.player.elements.inputs.volume,
|
||||||
inputEvent,
|
inputEvent,
|
||||||
event => {
|
event => {
|
||||||
@ -637,21 +637,21 @@ 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 => {
|
bind(getElements.call(this.player, 'input[type="range"]'), 'input', event => {
|
||||||
controls.updateRangeFill.call(this.player, event.target);
|
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',
|
'mousedown',
|
||||||
'touchstart',
|
'touchstart',
|
||||||
@ -659,11 +659,11 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 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 +672,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 +686,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 => {
|
||||||
|
@ -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,29 +17,29 @@ 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 { trigger } 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);
|
||||||
@ -184,7 +188,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 +216,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 +270,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);
|
trigger.call(this.player, this.player.media, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@ -393,7 +397,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 +534,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 +550,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 +581,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);
|
||||||
|
@ -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 { trigger } 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');
|
trigger.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,7 +70,7 @@ 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 ratio = is.string(input) ? input.split(':') : this.config.ratio.split(':');
|
||||||
const padding = 100 / ratio[0] * ratio[1];
|
const padding = 100 / ratio[0] * ratio[1];
|
||||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||||
|
|
||||||
@ -73,34 +99,34 @@ 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');
|
||||||
|
|
||||||
// Inject the package
|
// Inject the package
|
||||||
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
|
const wrapper = createElement('div', { 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +186,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');
|
trigger.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 +213,7 @@ const vimeo = {
|
|||||||
.setPlaybackRate(input)
|
.setPlaybackRate(input)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
speed = input;
|
speed = input;
|
||||||
utils.dispatchEvent.call(player, player.media, 'ratechange');
|
trigger.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 +233,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');
|
trigger.call(player, player.media, 'volumechange');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -219,11 +245,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');
|
trigger.call(player, player.media, 'volumechange');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -235,7 +261,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;
|
||||||
@ -272,7 +298,7 @@ const vimeo = {
|
|||||||
player.embed.getVideoWidth(),
|
player.embed.getVideoWidth(),
|
||||||
player.embed.getVideoHeight(),
|
player.embed.getVideoHeight(),
|
||||||
]).then(dimensions => {
|
]).then(dimensions => {
|
||||||
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
|
const ratio = 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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.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');
|
trigger.call(player, player.media, 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rebuild UI
|
// Rebuild UI
|
||||||
|
@ -4,7 +4,24 @@
|
|||||||
|
|
||||||
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 { trigger } 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) {
|
||||||
@ -57,11 +74,11 @@ function mapQualityUnit(input) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +88,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');
|
trigger.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 +132,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 +144,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 +170,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,23 +178,23 @@ 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 });
|
const container = createElement('div', { id });
|
||||||
player.media = utils.replaceElement(container, player.media);
|
player.media = replaceElement(container, player.media);
|
||||||
|
|
||||||
// Set poster image
|
// Set poster image
|
||||||
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)
|
||||||
@ -213,7 +229,7 @@ const youtube = {
|
|||||||
onError(event) {
|
onError(event) {
|
||||||
// If we've already fired an error, don't do it again
|
// If we've already fired an error, don't do it again
|
||||||
// YouTube fires onError twice
|
// YouTube fires onError twice
|
||||||
if (utils.is.object(player.media.error)) {
|
if (is.object(player.media.error)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,10 +266,10 @@ const youtube = {
|
|||||||
|
|
||||||
player.media.error = detail;
|
player.media.error = detail;
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'error');
|
trigger.call(player, player.media, 'error');
|
||||||
},
|
},
|
||||||
onPlaybackQualityChange() {
|
onPlaybackQualityChange() {
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
trigger.call(player, player.media, 'qualitychange', false, {
|
||||||
quality: player.media.quality,
|
quality: player.media.quality,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -264,7 +280,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');
|
trigger.call(player, player.media, 'ratechange');
|
||||||
},
|
},
|
||||||
onReady(event) {
|
onReady(event) {
|
||||||
// Get the instance
|
// Get the instance
|
||||||
@ -305,7 +321,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');
|
trigger.call(player, player.media, 'seeking');
|
||||||
|
|
||||||
// Seek after events sent
|
// Seek after events sent
|
||||||
instance.seekTo(time);
|
instance.seekTo(time);
|
||||||
@ -334,7 +350,7 @@ const youtube = {
|
|||||||
instance.setPlaybackQuality(mapQualityUnit(quality));
|
instance.setPlaybackQuality(mapQualityUnit(quality));
|
||||||
|
|
||||||
// Trigger request event
|
// Trigger request event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
trigger.call(player, player.media, 'qualityrequested', false, {
|
||||||
quality,
|
quality,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -349,7 +365,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');
|
trigger.call(player, player.media, 'volumechange');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -360,10 +376,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');
|
trigger.call(player, player.media, 'volumechange');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -389,8 +405,8 @@ const youtube = {
|
|||||||
player.media.setAttribute('tabindex', -1);
|
player.media.setAttribute('tabindex', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
trigger.call(player, player.media, 'timeupdate');
|
||||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
trigger.call(player, player.media, 'durationchange');
|
||||||
|
|
||||||
// Reset timer
|
// Reset timer
|
||||||
clearInterval(player.timers.buffering);
|
clearInterval(player.timers.buffering);
|
||||||
@ -402,7 +418,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');
|
trigger.call(player, player.media, 'progress');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set last buffer point
|
// Set last buffer point
|
||||||
@ -413,7 +429,7 @@ const youtube = {
|
|||||||
clearInterval(player.timers.buffering);
|
clearInterval(player.timers.buffering);
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
trigger.call(player, player.media, 'canplaythrough');
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
@ -435,7 +451,7 @@ const youtube = {
|
|||||||
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');
|
trigger.call(player, player.media, 'seeked');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
@ -448,11 +464,11 @@ const youtube = {
|
|||||||
switch (event.data) {
|
switch (event.data) {
|
||||||
case -1:
|
case -1:
|
||||||
// Update scrubber
|
// Update scrubber
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
trigger.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');
|
trigger.call(player, player.media, 'progress');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -465,7 +481,7 @@ const youtube = {
|
|||||||
instance.stopVideo();
|
instance.stopVideo();
|
||||||
instance.playVideo();
|
instance.playVideo();
|
||||||
} else {
|
} else {
|
||||||
utils.dispatchEvent.call(player, player.media, 'ended');
|
trigger.call(player, player.media, 'ended');
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -477,11 +493,11 @@ const youtube = {
|
|||||||
} else {
|
} else {
|
||||||
assurePlaybackState.call(player, true);
|
assurePlaybackState.call(player, true);
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
trigger.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');
|
trigger.call(player, player.media, 'timeupdate');
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
// Check duration again due to YouTube bug
|
// Check duration again due to YouTube bug
|
||||||
@ -489,7 +505,7 @@ 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');
|
trigger.call(player, player.media, 'durationchange');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get quality
|
// Get quality
|
||||||
@ -511,7 +527,7 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
|
trigger.call(player, player.elements.container, 'statechange', false, {
|
||||||
code: event.data,
|
code: event.data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
156
src/js/plyr.js
156
src/js/plyr.js
@ -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, toggleState, wrap } from './utils/elements';
|
||||||
|
import { off, on, trigger } from './utils/events';
|
||||||
|
import is from './utils/is';
|
||||||
|
import loadSprite from './utils/loadScript';
|
||||||
|
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,23 @@ 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) {
|
||||||
if (!utils.is.empty(params)) {
|
|
||||||
const truthy = [
|
const truthy = [
|
||||||
'1',
|
'1',
|
||||||
'true',
|
'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 +201,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;
|
||||||
}
|
}
|
||||||
@ -255,9 +259,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow focus to be captured
|
// Allow focus to be captured
|
||||||
@ -271,7 +275,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(this.elements.container, this.config.events.join(' '), event => {
|
||||||
this.debug.log(`event: ${event.type}`);
|
this.debug.log(`event: ${event.type}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -330,7 +334,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,7 +346,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,7 +387,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();
|
||||||
@ -399,7 +403,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,7 +420,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -424,7 +428,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -438,7 +442,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;
|
||||||
@ -461,7 +465,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,17 +509,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,7 +539,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -553,7 +557,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -562,7 +566,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -573,12 +577,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -624,15 +628,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,19 +675,19 @@ class Plyr {
|
|||||||
set quality(input) {
|
set quality(input) {
|
||||||
let quality = null;
|
let quality = null;
|
||||||
|
|
||||||
if (!utils.is.empty(input)) {
|
if (!is.empty(input)) {
|
||||||
quality = Number(input);
|
quality = Number(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(quality)) {
|
if (!is.number(quality)) {
|
||||||
quality = this.storage.get('quality');
|
quality = this.storage.get('quality');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(quality)) {
|
if (!is.number(quality)) {
|
||||||
quality = this.config.quality.selected;
|
quality = this.config.quality.selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(quality)) {
|
if (!is.number(quality)) {
|
||||||
quality = this.config.quality.default;
|
quality = this.config.quality.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,9 +696,9 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.options.quality.includes(quality)) {
|
if (!this.options.quality.includes(quality)) {
|
||||||
const closest = utils.closest(this.options.quality, quality);
|
const value = closest(this.options.quality, quality);
|
||||||
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
|
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
|
||||||
quality = closest;
|
quality = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update config
|
// Update config
|
||||||
@ -717,7 +721,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;
|
||||||
|
|
||||||
@ -816,7 +820,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -838,18 +842,18 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the method is called without parameter, toggle based on current value
|
// If the method is called without parameter, toggle based on current value
|
||||||
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
const active = is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
utils.toggleState(this.elements.buttons.captions, active);
|
toggleState(this.elements.buttons.captions, active);
|
||||||
|
|
||||||
// Add class hook
|
// Add class hook
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
|
toggleClass(this.elements.container, this.config.classNames.captions.active, active);
|
||||||
|
|
||||||
// Update state and trigger event
|
// Update state and trigger event
|
||||||
if (active !== this.captions.active) {
|
if (active !== this.captions.active) {
|
||||||
this.captions.active = active;
|
this.captions.active = active;
|
||||||
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
trigger.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -902,7 +906,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);
|
||||||
@ -938,22 +942,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);
|
trigger.call(this, this.media, eventName);
|
||||||
}
|
}
|
||||||
return !hiding;
|
return !hiding;
|
||||||
}
|
}
|
||||||
@ -966,7 +970,7 @@ 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(this.elements.container, event, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -975,7 +979,7 @@ class Plyr {
|
|||||||
* @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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1001,10 +1005,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;
|
||||||
@ -1014,7 +1018,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Callback
|
// Callback
|
||||||
if (utils.is.function(callback)) {
|
if (is.function(callback)) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1022,13 +1026,13 @@ class Plyr {
|
|||||||
this.listeners.clear();
|
this.listeners.clear();
|
||||||
|
|
||||||
// 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);
|
trigger.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1067,7 +1071,7 @@ class Plyr {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1117,7 +1121,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1128,15 +1132,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1144,6 +1148,6 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Plyr.defaults = utils.cloneDeep(defaults);
|
Plyr.defaults = cloneDeep(defaults);
|
||||||
|
|
||||||
export default Plyr;
|
export default Plyr;
|
||||||
|
@ -2,23 +2,24 @@
|
|||||||
// 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';
|
||||||
|
|
||||||
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 +27,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 (!is.object(input) || !('sources' in input) || !input.sources.length) {
|
||||||
this.debug.warn('Invalid source format');
|
this.debug.warn('Invalid source format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -42,17 +43,17 @@ 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;
|
this.type = input.type;
|
||||||
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
this.provider = !is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
||||||
|
|
||||||
// Check for support
|
// Check for support
|
||||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||||
@ -60,16 +61,16 @@ const source = {
|
|||||||
// Create new markup
|
// Create new markup
|
||||||
switch (`${this.provider}:${this.type}`) {
|
switch (`${this.provider}:${this.type}`) {
|
||||||
case 'html5:video':
|
case 'html5:video':
|
||||||
this.media = utils.createElement('video');
|
this.media = createElement('video');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'html5:audio':
|
case 'html5:audio':
|
||||||
this.media = utils.createElement('audio');
|
this.media = createElement('audio');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'youtube:video':
|
case 'youtube:video':
|
||||||
case 'vimeo:video':
|
case 'vimeo:video':
|
||||||
this.media = utils.createElement('div', {
|
this.media = createElement('div', {
|
||||||
src: input.sources[0].src,
|
src: input.sources[0].src,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -82,7 +83,7 @@ const source = {
|
|||||||
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 +95,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) {
|
||||||
|
@ -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));
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
// 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';
|
||||||
|
|
||||||
// Check for feature support
|
// Check for feature support
|
||||||
const support = {
|
const support = {
|
||||||
@ -15,7 +18,6 @@ const support = {
|
|||||||
check(type, provider, playsinline) {
|
check(type, provider, playsinline) {
|
||||||
let api = false;
|
let api = false;
|
||||||
let ui = false;
|
let ui = false;
|
||||||
const browser = utils.getBrowser();
|
|
||||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||||
|
|
||||||
switch (`${provider}:${type}`) {
|
switch (`${provider}:${type}`) {
|
||||||
@ -48,14 +50,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/
|
||||||
@ -69,7 +68,7 @@ const support = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Bail if no checking function
|
// Bail if no checking function
|
||||||
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
|
if (!this.isHTML5 || !is.function(media.canPlayType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,28 +118,6 @@ const support = {
|
|||||||
// 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 +130,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/
|
||||||
|
54
src/js/ui.js
54
src/js/ui.js
@ -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, toggleState } from './utils/elements';
|
||||||
// Sniff out the browser
|
import { trigger } 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,23 +86,23 @@ 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');
|
trigger.call(this, this.media, 'ready');
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Set the title
|
// Set the title
|
||||||
@ -125,7 +126,7 @@ 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}`;
|
||||||
|
|
||||||
// Set container label
|
// Set container label
|
||||||
@ -133,7 +134,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
@ -142,14 +143,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));
|
||||||
@ -158,7 +159,7 @@ 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)
|
||||||
@ -167,13 +168,12 @@ const ui = {
|
|||||||
this.media.setAttribute('poster', poster);
|
this.media.setAttribute('poster', poster);
|
||||||
|
|
||||||
// Bail if element is missing
|
// Bail if element is missing
|
||||||
if (!utils.is.element(this.elements.poster)) {
|
if (!is.element(this.elements.poster)) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the image, and set poster if successful
|
// Load the image, and set poster if successful
|
||||||
const loadPromise = utils.loadImage(poster)
|
const loadPromise = loadImage(poster).then(() => {
|
||||||
.then(() => {
|
|
||||||
this.elements.poster.style.backgroundImage = `url('${poster}')`;
|
this.elements.poster.style.backgroundImage = `url('${poster}')`;
|
||||||
Object.assign(this.elements.poster.style, {
|
Object.assign(this.elements.poster.style, {
|
||||||
backgroundImage: `url('${poster}')`,
|
backgroundImage: `url('${poster}')`,
|
||||||
@ -194,15 +194,15 @@ const ui = {
|
|||||||
// 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 ARIA state
|
// Set ARIA state
|
||||||
utils.toggleState(this.elements.buttons.play, this.playing);
|
toggleState(this.elements.buttons.play, this.playing);
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,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);
|
||||||
|
875
src/js/utils.js
875
src/js/utils.js
@ -1,875 +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);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Toggle aria-pressed state on a toggle button
|
|
||||||
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
|
|
||||||
toggleState(element, input) {
|
|
||||||
// If multiple elements passed
|
|
||||||
if (utils.is.array(element) || utils.is.nodeList(element)) {
|
|
||||||
Array.from(element).forEach(target => utils.toggleState(target, input));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bail if no target
|
|
||||||
if (!utils.is.element(element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get state
|
|
||||||
const pressed = element.getAttribute('aria-pressed') === 'true';
|
|
||||||
const state = utils.is.boolean(input) ? input : !pressed;
|
|
||||||
|
|
||||||
// Set the attribute on target
|
|
||||||
element.setAttribute('aria-pressed', state);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Format string
|
|
||||||
format(input, ...args) {
|
|
||||||
if (utils.is.empty(input)) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get percentage
|
|
||||||
getPercentage(current, max) {
|
|
||||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
|
||||||
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
src/js/utils/animation.js
Normal file
30
src/js/utils/animation.js
Normal 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
src/js/utils/arrays.js
Normal file
23
src/js/utils/arrays.js
Normal 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
src/js/utils/browser.js
Normal file
13
src/js/utils/browser.js
Normal 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;
|
307
src/js/utils/elements.js
Normal file
307
src/js/utils/elements.js
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Element utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import { off, on } 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(attributes).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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (toggle) {
|
||||||
|
on(this.elements.container, 'keydown', trap, false);
|
||||||
|
} else {
|
||||||
|
off(this.elements.container, 'keydown', trap, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle aria-pressed state on a toggle button
|
||||||
|
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
|
||||||
|
export function toggleState(element, input) {
|
||||||
|
// If multiple elements passed
|
||||||
|
if (is.array(element) || is.nodeList(element)) {
|
||||||
|
Array.from(element).forEach(target => toggleState(target, input));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail if no target
|
||||||
|
if (!is.element(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get state
|
||||||
|
const pressed = element.getAttribute('aria-pressed') === 'true';
|
||||||
|
const state = is.boolean(input) ? input : !pressed;
|
||||||
|
|
||||||
|
// Set the attribute on target
|
||||||
|
element.setAttribute('aria-pressed', state);
|
||||||
|
}
|
98
src/js/utils/events.js
Normal file
98
src/js/utils/events.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// 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(elements, event, callback, toggle = false, passive = true, capture = false) {
|
||||||
|
// Bail if no elemetns, event, or callback
|
||||||
|
if (is.empty(elements) || is.empty(event) || !is.function(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a nodelist is passed, call itself on each node
|
||||||
|
if (is.nodeList(elements) || is.array(elements)) {
|
||||||
|
// Create listener for each node
|
||||||
|
Array.from(elements).forEach(element => {
|
||||||
|
if (element instanceof Node) {
|
||||||
|
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 (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 => {
|
||||||
|
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind event handler
|
||||||
|
export function on(element, events = '', callback, passive = true, capture = false) {
|
||||||
|
toggleListener(element, events, callback, true, passive, capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbind event handler
|
||||||
|
export function off(element, events = '', callback, passive = true, capture = false) {
|
||||||
|
toggleListener(element, events, callback, false, passive, capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger event
|
||||||
|
export function trigger(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);
|
||||||
|
}
|
42
src/js/utils/fetch.js
Normal file
42
src/js/utils/fetch.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
64
src/js/utils/is.js
Normal file
64
src/js/utils/is.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// 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
src/js/utils/loadImage.js
Normal file
19
src/js/utils/loadImage.js
Normal 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
src/js/utils/loadScript.js
Normal file
14
src/js/utils/loadScript.js
Normal 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
src/js/utils/loadSprite.js
Normal file
75
src/js/utils/loadSprite.js
Normal 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
src/js/utils/objects.js
Normal file
42
src/js/utils/objects.js
Normal 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);
|
||||||
|
}
|
82
src/js/utils/strings.js
Normal file
82
src/js/utils/strings.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// 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) => (is.string(args[i]) ? args[i] : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
src/js/utils/time.js
Normal file
36
src/js/utils/time.js
Normal 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 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
||||||
|
}
|
44
src/js/utils/urls.js
Normal file
44
src/js/utils/urls.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// 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) {
|
||||||
|
if (!is.object(input)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(input).forEach(([
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
]) => {
|
||||||
|
params.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user