Utils broken down into seperate files and exports

This commit is contained in:
Sam Potts
2018-06-13 00:02:55 +10:00
parent 840e31a693
commit 392dfd024c
42 changed files with 5282 additions and 5316 deletions
+1872 -1924
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1870 -1920
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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"
} }
} }
+2 -1
View File
@@ -11,7 +11,8 @@
}, },
// Exclude from search // Exclude from search
"search.exclude": { "search.exclude": {
"dist/": true "dist/": true,
"demo/dist/": true
}, },
// Linting // Linting
"stylelint.enable": true, "stylelint.enable": true,
+49 -51
View File
@@ -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,21 +96,19 @@ 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)) this.debug.log('Track added', track);
.forEach(track => { // Attempt to store if the original dom element was "default"
this.debug.log('Track added', track); meta.set(track, {
// Attempt to store if the original dom element was "default" default: track.mode === 'showing',
meta.set(track, {
default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
}); });
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
on(track, 'cuechange', () => captions.updateCues.call(this));
});
} }
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
@@ -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,12 +202,10 @@ 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)) 'captions',
.filter(track => [ 'subtitles',
'captions', ].includes(track.kind));
'subtitles',
].includes(track.kind));
}, },
// Get the current track for the current language // Get the current track for the current language
@@ -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 };
+161 -159
View File
@@ -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(
class: `plyr__time ${attributes.class}`, 'div',
'aria-label': i18n.get(type, this.config), extend(attributes, {
}), '00:00'); class: `plyr__time ${attributes.class}`,
'aria-label': i18n.get(type, this.config),
}),
'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}&times;`; return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
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');
}); });
} }
+18 -17
View File
@@ -3,9 +3,10 @@
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing // https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ========================================================================== // ==========================================================================
import utils from './utils'; import browser from './utils/browser';
import { hasClass, toggleClass, 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}`]();
} }
+16 -13
View File
@@ -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
+7 -5
View File
@@ -2,17 +2,19 @@
// Plyr internationalization // Plyr internationalization
// ========================================================================== // ==========================================================================
import utils from './utils'; import is from './utils/is';
import { getDeep } from './utils/objects';
import { replaceAll } from './utils/strings';
const i18n = { const i18n = {
get(key = '', config = {}) { get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config)) { if (is.empty(key) || is.empty(config)) {
return ''; return '';
} }
let string = utils.getDeep(config.i18n, key); let string = getDeep(config.i18n, key);
if (utils.is.empty(string)) { if (is.empty(string)) {
return ''; return '';
} }
@@ -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;
+76 -76
View File
@@ -4,10 +4,10 @@
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
// Sniff out the browser import { 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 => {
+7 -7
View File
@@ -5,7 +5,7 @@
import html5 from './html5'; import html5 from './html5';
import vimeo from './plugins/vimeo'; import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube'; import youtube from './plugins/youtube';
import utils from './utils'; import { createElement, toggleClass, wrap } from './utils/elements';
const media = { const media = {
// Setup media // Setup media
@@ -17,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,
}); });
+21 -17
View File
@@ -7,7 +7,12 @@
/* global google */ /* global google */
import i18n from '../i18n'; import i18n from '../i18n';
import utils from '../utils'; import { createElement } from './../utils/elements';
import { 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);
+62 -36
View File
@@ -5,7 +5,34 @@
import captions from './../captions'; import captions from './../captions';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils'; import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { 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
+60 -44
View File
@@ -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,
}); });
}, },
+80 -76
View File
@@ -6,9 +6,10 @@
// ========================================================================== // ==========================================================================
import captions from './captions'; import captions from './captions';
import defaults from './config/defaults';
import { getProviderByUrl, providers, types } from './config/types';
import Console from './console'; import Console from './console';
import controls from './controls'; import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen'; import Fullscreen from './fullscreen';
import Listeners from './listeners'; import Listeners from './listeners';
import media from './media'; import media from './media';
@@ -16,9 +17,14 @@ import Ads from './plugins/ads';
import source from './source'; import source from './source';
import Storage from './storage'; import Storage from './storage';
import support from './support'; import support from './support';
import { providers, types } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, 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;
+16 -15
View File
@@ -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) {
+7 -6
View File
@@ -2,7 +2,8 @@
// Plyr storage // Plyr storage
// ========================================================================== // ==========================================================================
import utils from './utils'; import is from './utils/is';
import { extend } from './utils/objects';
class Storage { class Storage {
constructor(player) { constructor(player) {
@@ -37,13 +38,13 @@ class Storage {
const store = window.localStorage.getItem(this.key); const store = window.localStorage.getItem(this.key);
if (utils.is.empty(store)) { if (is.empty(store)) {
return null; return null;
} }
const json = JSON.parse(store); const json = JSON.parse(store);
return utils.is.string(key) && key.length ? json[key] : json; return is.string(key) && key.length ? json[key] : json;
} }
set(object) { set(object) {
@@ -53,7 +54,7 @@ class Storage {
} }
// Can only store objectst // Can only store objectst
if (!utils.is.object(object)) { if (!is.object(object)) {
return; return;
} }
@@ -61,12 +62,12 @@ class Storage {
let storage = this.get(); let storage = this.get();
// Default to empty object // Default to empty object
if (utils.is.empty(storage)) { if (is.empty(storage)) {
storage = {}; storage = {};
} }
// Update the working copy of the values // Update the working copy of the values
utils.extend(storage, object); extend(storage, object);
// Update storage // Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage)); window.localStorage.setItem(this.key, JSON.stringify(storage));
+8 -31
View File
@@ -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/
+35 -35
View File
@@ -6,15 +6,16 @@ import captions from './captions';
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support'; import support from './support';
import utils from './utils'; import browser from './utils/browser';
import { getElement, toggleClass, 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,22 +168,21 @@ 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}')`, // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) backgroundSize: '',
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
}); });
ui.togglePoster.call(this, true);
return poster;
});
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
loadPromise.catch(() => ui.togglePoster.call(this, false)); loadPromise.catch(() => ui.togglePoster.call(this, false));
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,30 @@
// ==========================================================================
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false;
})();
// Force repaint of element
export function repaint(element) {
setTimeout(() => {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
}, 0);
}
+23
View File
@@ -0,0 +1,23 @@
// ==========================================================================
// Array utils
// ==========================================================================
import is from './is';
// Remove duplicates in an array
export function dedupe(array) {
if (!is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
}
// Get the closest value in an array
export function closest(array, value) {
if (!is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
+13
View File
@@ -0,0 +1,13 @@
// ==========================================================================
// Browser sniffing
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
export default browser;
+307
View 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
View 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
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Fetch wrapper
// Using XHR to avoid issues with older browsers
// ==========================================================================
export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
}
+64
View 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
View File
@@ -0,0 +1,19 @@
// ==========================================================================
// Load image avoiding xhr/fetch CORS issues
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
// By default it checks if it is at least 1px, but you can add a second argument to change this
// ==========================================================================
export default function loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, { onload: handler, onerror: handler, src });
});
}
+14
View File
@@ -0,0 +1,14 @@
// ==========================================================================
// Load an external script
// ==========================================================================
import loadjs from 'loadjs';
export default function loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
}
+75
View File
@@ -0,0 +1,75 @@
// ==========================================================================
// Sprite loader
// ==========================================================================
import Storage from './../storage';
import is from './is';
// Load an external SVG sprite
export default function loadSprite(url, id) {
if (!is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Object utils
// ==========================================================================
import is from './is';
// Clone nested objects
export function cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
}
// Get a nested value in an object
export function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
}
// Deep extend destination object with N more objects
export function extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return extend(target, ...sources);
}
+82
View 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
View File
@@ -0,0 +1,36 @@
// ==========================================================================
// Time utils
// ==========================================================================
import is from './is';
// Time helpers
export const getHours = value => parseInt((value / 60 / 60) % 60, 10);
export const getMinutes = value => parseInt((value / 60) % 60, 10);
export const getSeconds = value => parseInt(value % 60, 10);
// Format time to UI friendly string
export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
const secs = getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}
+44
View 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;
}