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

3348
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3342
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-postcss": "^7.0.1",
"gulp-rename": "^1.2.3",
"gulp-rename": "^1.3.0",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1",
@ -74,7 +74,7 @@
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4",
"raven-js": "^3.26.1",
"raven-js": "^3.26.2",
"url-polyfill": "^1.0.13"
}
}

View File

@ -11,7 +11,8 @@
},
// Exclude from search
"search.exclude": {
"dist/": true
"dist/": true,
"demo/dist/": true
},
// Linting
"stylelint.enable": true,

View File

@ -6,7 +6,13 @@
import controls from './controls';
import i18n from './i18n';
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 = {
// Setup captions
@ -19,7 +25,7 @@ const captions = {
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// 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);
}
@ -27,15 +33,12 @@ const captions = {
}
// Inject the container
if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
if (!is.element(this.elements.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
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
@ -43,19 +46,18 @@ const captions = {
Array.from(elements).forEach(track => {
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:',
'https:',
].includes(href.protocol)) {
utils
.fetch(src, 'blob')
].includes(url.protocol)) {
fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
utils.removeElement(track);
removeElement(track);
});
}
});
@ -65,7 +67,7 @@ const captions = {
let active = this.storage.get('captions');
// Otherwise fall back to the default config
if (!utils.is.boolean(active)) {
if (!is.boolean(active)) {
({ active } = this.config.captions);
}
@ -80,7 +82,7 @@ const captions = {
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this));
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)
@ -94,9 +96,7 @@ const captions = {
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks
.filter(track => !meta.get(track))
.forEach(track => {
tracks.filter(track => !meta.get(track)).forEach(track => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
@ -107,7 +107,7 @@ const captions = {
track.mode = 'hidden';
// Add event listener for cue changes
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
on(track, 'cuechange', () => captions.updateCues.call(this));
});
}
@ -120,7 +120,7 @@ const captions = {
}
// Enable or disable captions based on track length
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
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
@ -137,7 +137,7 @@ const captions = {
return;
}
if (!utils.is.number(index)) {
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
@ -166,7 +166,7 @@ const captions = {
}
// Trigger event
utils.dispatchEvent.call(this, this.media, 'languagechange');
trigger.call(this, this.media, 'languagechange');
}
if (this.isHTML5 && this.isVideo) {
@ -181,7 +181,7 @@ const captions = {
},
setLanguage(language, show = true) {
if (!utils.is.string(language)) {
if (!is.string(language)) {
this.debug.warn('Invalid language argument', language);
return;
}
@ -202,9 +202,7 @@ const captions = {
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => [
return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => [
'captions',
'subtitles',
].includes(track.kind));
@ -222,16 +220,16 @@ const captions = {
getLabel(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);
}
if (utils.is.track(currentTrack)) {
if (!utils.is.empty(currentTrack.label)) {
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
if (!utils.is.empty(currentTrack.language)) {
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
@ -249,13 +247,13 @@ const captions = {
return;
}
if (!utils.is.element(this.elements.captions)) {
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
return;
}
// 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);
return;
}
@ -267,7 +265,7 @@ const captions = {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(utils.getHTML);
.map(getHTML);
}
// Set new caption text
@ -276,13 +274,13 @@ const captions = {
if (changed) {
// Empty the container and create a new child element
utils.emptyElement(this.elements.captions);
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
utils.dispatchEvent.call(this, this.media, 'cuechange');
trigger.call(this, this.media, 'cuechange');
}
},
};

View File

@ -13,4 +13,22 @@ export const types = {
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 };

314
src/js/controls.js vendored
View File

@ -6,14 +6,17 @@ import captions from './captions';
import html5 from './html5';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
import { repaint, transitionEndEvent } from './utils/animation';
import browser from './utils/browser';
import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, removeElement, setAttributes, toggleClass, toggleHidden, toggleState } from './utils/elements';
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 = {
// Get icon URL
getIconUrl() {
const url = new URL(this.config.iconUrl, window.location);
@ -29,41 +32,41 @@ const controls = {
// TODO: Allow settings menus with custom controls
findElements() {
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
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
play: getElements.call(this, this.config.selectors.buttons.play),
pause: getElement.call(this, this.config.selectors.buttons.pause),
restart: getElement.call(this, this.config.selectors.buttons.restart),
rewind: getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),
mute: getElement.call(this, this.config.selectors.buttons.mute),
pip: getElement.call(this, this.config.selectors.buttons.pip),
airplay: getElement.call(this, this.config.selectors.buttons.airplay),
settings: getElement.call(this, this.config.selectors.buttons.settings),
captions: getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
this.elements.progress = getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
seek: getElement.call(this, this.config.selectors.inputs.seek),
volume: getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
buffer: getElement.call(this, this.config.selectors.display.buffer),
currentTime: getElement.call(this, this.config.selectors.display.currentTime),
duration: getElement.call(this, this.config.selectors.display.duration),
};
// 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}`);
}
@ -87,9 +90,9 @@ const controls = {
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
utils.setAttributes(
setAttributes(
icon,
utils.extend(attributes, {
extend(attributes, {
role: 'presentation',
focusable: 'false',
}),
@ -138,21 +141,21 @@ const controls = {
attributes.class = this.config.classNames.hidden;
}
return utils.createElement('span', attributes, text);
return createElement('span', attributes, text);
},
// Create a badge
createBadge(text) {
if (utils.is.empty(text)) {
if (is.empty(text)) {
return null;
}
const badge = utils.createElement('span', {
const badge = createElement('span', {
class: this.config.classNames.menu.value,
});
badge.appendChild(
utils.createElement(
createElement(
'span',
{
class: this.config.classNames.menu.badge,
@ -166,9 +169,9 @@ const controls = {
// Create a <button>
createButton(buttonType, attr) {
const button = utils.createElement('button');
const button = createElement('button');
const attributes = Object.assign({}, attr);
let type = utils.toCamelCase(buttonType);
let type = toCamelCase(buttonType);
let toggle = false;
let label;
@ -252,13 +255,13 @@ const controls = {
}
// 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
if (type === 'play') {
if (!utils.is.array(this.elements.buttons[type])) {
if (!is.array(this.elements.buttons[type])) {
this.elements.buttons[type] = [];
}
@ -273,7 +276,7 @@ const controls = {
// Create an <input type='range'>
createRange(type, attributes) {
// Seek label
const label = utils.createElement(
const label = createElement(
'label',
{
for: attributes.id,
@ -284,10 +287,10 @@ const controls = {
);
// Seek input
const input = utils.createElement(
const input = createElement(
'input',
utils.extend(
utils.getAttributesFromSelector(this.config.selectors.inputs[type]),
extend(
getAttributesFromSelector(this.config.selectors.inputs[type]),
{
type: 'range',
min: 0,
@ -319,10 +322,10 @@ const controls = {
// Create a <progress>
createProgress(type, attributes) {
const progress = utils.createElement(
const progress = createElement(
'progress',
utils.extend(
utils.getAttributesFromSelector(this.config.selectors.display[type]),
extend(
getAttributesFromSelector(this.config.selectors.display[type]),
{
min: 0,
max: 100,
@ -336,7 +339,7 @@ const controls = {
// Create the label inside
if (type !== 'volume') {
progress.appendChild(utils.createElement('span', null, '0'));
progress.appendChild(createElement('span', null, '0'));
let suffix = '';
switch (type) {
@ -362,12 +365,16 @@ const controls = {
// Create time display
createTime(type) {
const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
const attributes = getAttributesFromSelector(this.config.selectors.display[type]);
const container = utils.createElement('div', utils.extend(attributes, {
const container = createElement(
'div',
extend(attributes, {
class: `plyr__time ${attributes.class}`,
'aria-label': i18n.get(type, this.config),
}), '00:00');
}),
'00:00',
);
// Reference for updates
this.elements.display[type] = container;
@ -377,15 +384,15 @@ const controls = {
// Create a settings menu item
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,
});
const radio = utils.createElement(
const radio = createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs[type]), {
extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'radio',
name: `plyr-${type}`,
value,
@ -394,13 +401,13 @@ const controls = {
}),
);
const faux = utils.createElement('span', { hidden: '' });
const faux = createElement('span', { hidden: '' });
label.appendChild(radio);
label.appendChild(faux);
label.insertAdjacentHTML('beforeend', title);
if (utils.is.element(badge)) {
if (is.element(badge)) {
label.appendChild(badge);
}
@ -411,15 +418,15 @@ const controls = {
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target) || !utils.is.number(time)) {
if (!is.element(target) || !is.number(time)) {
return;
}
// 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
target.innerText = utils.formatTime(time, forceHours, inverted);
target.innerText = formatTime(time, forceHours, inverted);
},
// Update volume UI and storage
@ -429,19 +436,19 @@ const controls = {
}
// 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);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
if (is.element(this.elements.buttons.mute)) {
toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
if (!is.element(target)) {
return;
}
@ -454,23 +461,23 @@ const controls = {
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
if (!this.supported.ui || !is.event(event)) {
return;
}
let value = 0;
const setProgress = (target, input) => {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
const value = is.number(input) ? input : 0;
const progress = is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
if (is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
if (is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
@ -482,7 +489,7 @@ const controls = {
case 'timeupdate':
case 'seeking':
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
if (event.type === 'timeupdate') {
@ -507,10 +514,10 @@ const controls = {
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// Get range from event if event passed
const range = utils.is.event(target) ? target.target : target;
const range = is.event(target) ? target.target : target;
// 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;
}
@ -529,12 +536,7 @@ const controls = {
// Update hover tooltip for seeking
updateSeekTooltip(event) {
// Bail if setting not true
if (
!this.config.tooltips.seek ||
!utils.is.element(this.elements.inputs.seek) ||
!utils.is.element(this.elements.display.seekTooltip) ||
this.duration === 0
) {
if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {
return;
}
@ -544,7 +546,7 @@ const controls = {
const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => {
utils.toggleClass(this.elements.display.seekTooltip, visible, toggle);
toggleClass(this.elements.display.seekTooltip, visible, toggle);
};
// Hide on touch
@ -554,9 +556,9 @@ const controls = {
}
// Determine percentage, if already visible
if (utils.is.event(event)) {
if (is.event(event)) {
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);
} else {
return;
@ -577,7 +579,7 @@ const controls = {
// Show/hide the tooltip
// If the event is a moues in/out and percentage is inside bounds
if (utils.is.event(event) && [
if (is.event(event) && [
'mouseenter',
'mouseleave',
].includes(event.type)) {
@ -588,7 +590,7 @@ const controls = {
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
const invert = !is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
@ -610,7 +612,7 @@ const controls = {
}
// 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 (!hasDuration && this.config.displayDuration && this.paused) {
@ -628,14 +630,14 @@ const controls = {
// Hide/show a tab
toggleTab(setting, toggle) {
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
toggleHidden(this.elements.settings.tabs[setting], !toggle);
},
// Set the quality menu
// TODO: Vimeo support
setQualityMenu(options) {
// Menu required
if (!utils.is.element(this.elements.settings.panes.quality)) {
if (!is.element(this.elements.settings.panes.quality)) {
return;
}
@ -643,12 +645,12 @@ const controls = {
const list = this.elements.settings.panes.quality.querySelector('ul');
// 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));
}
// 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);
// Check if we need to toggle the parent
@ -660,7 +662,7 @@ const controls = {
}
// Empty the menu
utils.emptyElement(list);
emptyElement(list);
// Get the badge HTML for HD, 4K etc
const getBadge = quality => {
@ -699,7 +701,7 @@ const controls = {
return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
case 'quality':
if (utils.is.number(value)) {
if (is.number(value)) {
const label = i18n.get(`qualityLabel.${value}`, this.config);
if (!label.length) {
@ -709,7 +711,7 @@ const controls = {
return label;
}
return utils.toTitleCase(value);
return toTitleCase(value);
case 'captions':
return captions.getLabel.call(this);
@ -731,15 +733,15 @@ const controls = {
break;
default:
value = !utils.is.empty(input) ? input : this[setting];
value = !is.empty(input) ? input : this[setting];
// Get default
if (utils.is.empty(value)) {
if (is.empty(value)) {
value = this.config[setting].default;
}
// 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}`);
return;
}
@ -754,12 +756,12 @@ const controls = {
}
// Get the list if we need to
if (!utils.is.element(list)) {
if (!is.element(list)) {
list = pane && pane.querySelector('ul');
}
// If there's no list it means it's not been rendered...
if (!utils.is.element(list)) {
if (!is.element(list)) {
return;
}
@ -770,7 +772,7 @@ const controls = {
// Find the radio option and check it
const target = list && list.querySelector(`input[value="${value}"]`);
if (utils.is.element(target)) {
if (is.element(target)) {
target.checked = true;
}
},
@ -778,7 +780,7 @@ const controls = {
// Set the looping options
/* setLoopMenu() {
// Menu required
if (!utils.is.element(this.elements.settings.panes.loop)) {
if (!is.element(this.elements.settings.panes.loop)) {
return;
}
@ -786,22 +788,22 @@ const controls = {
const list = this.elements.settings.panes.loop.querySelector('ul');
// Show the pane and tab
utils.toggleHidden(this.elements.settings.tabs.loop, false);
utils.toggleHidden(this.elements.settings.panes.loop, false);
toggleHidden(this.elements.settings.tabs.loop, false);
toggleHidden(this.elements.settings.panes.loop, false);
// 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);
// Empty the menu
utils.emptyElement(list);
emptyElement(list);
options.forEach(option => {
const item = utils.createElement('li');
const item = createElement('li');
const button = utils.createElement(
const button = createElement(
'button',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.loop), {
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button',
class: this.config.classNames.control,
'data-plyr-loop-action': option,
@ -833,7 +835,7 @@ const controls = {
controls.toggleTab.call(this, type, tracks.length);
// Empty the menu
utils.emptyElement(list);
emptyElement(list);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
@ -876,14 +878,14 @@ const controls = {
}
// Menu required
if (!utils.is.element(this.elements.settings.panes.speed)) {
if (!is.element(this.elements.settings.panes.speed)) {
return;
}
const type = 'speed';
// Set the speed options
if (utils.is.array(options)) {
if (is.array(options)) {
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [
@ -901,7 +903,7 @@ const controls = {
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// 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);
// Check if we need to toggle the parent
@ -916,7 +918,7 @@ const controls = {
const list = this.elements.settings.panes.speed.querySelector('ul');
// Empty the menu
utils.emptyElement(list);
emptyElement(list);
// Create items
this.options.speed.forEach(speed => {
@ -934,9 +936,9 @@ const controls = {
// Check if we need to hide/show the settings menu
checkMenu() {
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
@ -945,14 +947,14 @@ const controls = {
const button = this.elements.buttons.settings;
// Menu and button are required
if (!utils.is.element(form) || !utils.is.element(button)) {
if (!is.element(form) || !is.element(button)) {
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)) {
const isMenuItem = utils.is.element(form) && form.contains(event.target);
if (is.event(event)) {
const isMenuItem = is.element(form) && form.contains(event.target);
const isButton = event.target === this.elements.buttons.settings;
// If the click was inside the form or if the click
@ -969,13 +971,13 @@ const controls = {
}
// Set form and button attributes
if (utils.is.element(button)) {
if (is.element(button)) {
button.setAttribute('aria-expanded', show);
}
if (utils.is.element(form)) {
utils.toggleHidden(form, !show);
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (is.element(form)) {
toggleHidden(form, !show);
toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) {
form.removeAttribute('tabindex');
@ -1006,7 +1008,7 @@ const controls = {
const height = clone.scrollHeight;
// Remove from the DOM
utils.removeElement(clone);
removeElement(clone);
return {
width,
@ -1020,7 +1022,7 @@ const controls = {
const pane = document.getElementById(target);
// Nothing to show, bail
if (!utils.is.element(pane)) {
if (!is.element(pane)) {
return;
}
@ -1064,11 +1066,11 @@ const controls = {
container.style.height = '';
// Only listen once
utils.off(container, utils.transitionEndEvent, restore);
off(container, transitionEndEvent, restore);
};
// Listen for the transition finishing and restore auto height/width
utils.on(container, utils.transitionEndEvent, restore);
on(container, transitionEndEvent, restore);
// Set dimensions to target
container.style.width = `${size.width}px`;
@ -1076,13 +1078,13 @@ const controls = {
}
// Set attributes on current tab
utils.toggleHidden(current, true);
toggleHidden(current, true);
current.setAttribute('tabindex', -1);
// 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 => {
tab.setAttribute('aria-expanded', true);
});
@ -1096,12 +1098,12 @@ const controls = {
// TODO: Set order based on order in the config.controls array?
create(data) {
// Do nothing if we want no controls
if (utils.is.empty(this.config.controls)) {
if (is.empty(this.config.controls)) {
return null;
}
// 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
if (this.config.controls.includes('restart')) {
@ -1125,7 +1127,7 @@ const controls = {
// 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
const seek = controls.createRange.call(this, 'seek', {
@ -1141,7 +1143,7 @@ const controls = {
// Seek tooltip
if (this.config.tooltips.seek) {
const tooltip = utils.createElement(
const tooltip = createElement(
'span',
{
class: this.config.classNames.tooltip,
@ -1174,7 +1176,7 @@ const controls = {
// Volume range control
if (this.config.controls.includes('volume')) {
const volume = utils.createElement('div', {
const volume = createElement('div', {
class: 'plyr__volume',
});
@ -1189,7 +1191,7 @@ const controls = {
const range = controls.createRange.call(
this,
'volume',
utils.extend(attributes, {
extend(attributes, {
id: `plyr-volume-${data.id}`,
}),
);
@ -1207,8 +1209,8 @@ const controls = {
}
// Settings button / menu
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
const menu = utils.createElement('div', {
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
const menu = createElement('div', {
class: 'plyr__menu',
hidden: '',
});
@ -1222,7 +1224,7 @@ const controls = {
}),
);
const form = utils.createElement('form', {
const form = createElement('form', {
class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`,
hidden: '',
@ -1231,29 +1233,29 @@ const controls = {
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`,
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tabpanel',
});
// Create the tab list
const tabs = utils.createElement('ul', {
const tabs = createElement('ul', {
role: 'tablist',
});
// Build the tabs
this.config.settings.forEach(type => {
const tab = utils.createElement('li', {
const tab = createElement('li', {
role: 'tab',
hidden: '',
});
const button = utils.createElement(
const button = createElement(
'button',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.settings), {
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
id: `plyr-settings-${data.id}-${type}-tab`,
@ -1264,7 +1266,7 @@ const controls = {
i18n.get(type, this.config),
);
const value = utils.createElement('span', {
const value = createElement('span', {
class: this.config.classNames.menu.value,
});
@ -1283,7 +1285,7 @@ const controls = {
// Build the panes
this.config.settings.forEach(type => {
const pane = utils.createElement('div', {
const pane = createElement('div', {
id: `plyr-settings-${data.id}-${type}`,
hidden: '',
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
@ -1291,7 +1293,7 @@ const controls = {
tabindex: -1,
});
const back = utils.createElement(
const back = createElement(
'button',
{
type: 'button',
@ -1305,7 +1307,7 @@ const controls = {
pane.appendChild(back);
const options = utils.createElement('ul');
const options = createElement('ul');
pane.appendChild(options);
inner.appendChild(pane);
@ -1360,7 +1362,7 @@ const controls = {
// Only load external sprite using AJAX
if (icon.cors) {
utils.loadSprite(icon.url, 'sprite-plyr');
loadSprite(icon.url, 'sprite-plyr');
}
}
@ -1379,10 +1381,10 @@ const controls = {
};
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
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
// The function can return a HTMLElement or String
container = this.config.controls.call(this, props);
@ -1408,7 +1410,7 @@ const controls = {
key,
value,
]) => {
result = utils.replaceAll(result, `{${key}}`, value);
result = replaceAll(result, `{${key}}`, value);
});
return result;
@ -1416,9 +1418,9 @@ const controls = {
// Update markup
if (update) {
if (utils.is.string(this.config.controls)) {
if (is.string(this.config.controls)) {
container = replace(container);
} else if (utils.is.element(container)) {
} else if (is.element(container)) {
container.innerHTML = replace(container.innerHTML);
}
}
@ -1427,35 +1429,35 @@ const controls = {
let target;
// 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);
}
// Inject into the container by default
if (!utils.is.element(target)) {
if (!is.element(target)) {
target = this.elements.container;
}
// Inject controls HTML
if (utils.is.element(container)) {
if (is.element(container)) {
target.appendChild(container);
} else if (container) {
target.insertAdjacentHTML('beforeend', container);
}
// Find the elements if need be
if (!utils.is.element(this.elements.controls)) {
if (!is.element(this.elements.controls)) {
controls.findElements.call(this);
}
// Edge sometimes doesn't finish the paint so force a redraw
if (window.navigator.userAgent.includes('Edge')) {
utils.repaint(target);
repaint(target);
}
// Setup tooltips
if (this.config.tooltips.controls) {
const labels = utils.getElements.call(
const labels = getElements.call(
this,
[
this.config.selectors.controls.wrapper,
@ -1467,8 +1469,8 @@ const controls = {
);
Array.from(labels).forEach(label => {
utils.toggleClass(label, this.config.classNames.hidden, false);
utils.toggleClass(label, this.config.classNames.tooltip, true);
toggleClass(label, this.config.classNames.hidden, false);
toggleClass(label, this.config.classNames.tooltip, true);
label.setAttribute('role', 'tooltip');
});
}

View File

@ -3,9 +3,10 @@
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ==========================================================================
import utils from './utils';
const browser = utils.getBrowser();
import browser from './utils/browser';
import { hasClass, toggleClass, toggleState, trapFocus } from './utils/elements';
import { on, trigger } from './utils/events';
import is from './utils/is';
function onChange() {
if (!this.enabled) {
@ -14,16 +15,16 @@ function onChange() {
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (utils.is.element(button)) {
utils.toggleState(button, this.active);
if (is.element(button)) {
toggleState(button, this.active);
}
// 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
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' : '';
// 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
onChange.call(this);
@ -62,15 +63,15 @@ class Fullscreen {
// Register event listeners
// 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??
onChange.call(this);
});
// 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
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;
}
@ -89,7 +90,7 @@ class Fullscreen {
// Get the prefix for handlers
static get prefix() {
// No prefix
if (utils.is.function(document.exitFullscreen)) {
if (is.function(document.exitFullscreen)) {
return '';
}
@ -102,7 +103,7 @@ class Fullscreen {
];
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;
return true;
}
@ -135,7 +136,7 @@ class Fullscreen {
// Fallback using classname
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`];
@ -157,7 +158,7 @@ class Fullscreen {
}
// 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
@ -175,7 +176,7 @@ class Fullscreen {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) {
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
@ -194,7 +195,7 @@ class Fullscreen {
toggleFallback.call(this, false);
} else if (!this.prefix) {
(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';
document[`${this.prefix}${action}${this.property}`]();
}

View File

@ -3,7 +3,10 @@
// ==========================================================================
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 = {
getSources() {
@ -23,20 +26,20 @@ const html5 = {
// Get sources
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
if (is.empty(sources)) {
return null;
}
// 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 (utils.is.empty(sizes)) {
if (is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
return dedupe(sizes.map(source => Number(source.getAttribute('size'))));
},
extend() {
@ -52,13 +55,13 @@ const html5 = {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
if (is.empty(sources)) {
return null;
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
if (is.empty(matches)) {
return null;
}
@ -68,7 +71,7 @@ const html5 = {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
if (is.empty(sources)) {
return;
}
@ -76,7 +79,7 @@ const html5 = {
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
if (is.empty(matches)) {
return;
}
@ -84,12 +87,12 @@ const html5 = {
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
if (is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
trigger.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
@ -115,7 +118,7 @@ const html5 = {
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
trigger.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
@ -130,7 +133,7 @@ const html5 = {
}
// Remove child sources
utils.removeElement(html5.getSources());
removeElement(html5.getSources());
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error

View File

@ -2,17 +2,19 @@
// Plyr internationalization
// ==========================================================================
import utils from './utils';
import is from './utils/is';
import { getDeep } from './utils/objects';
import { replaceAll } from './utils/strings';
const i18n = {
get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config)) {
if (is.empty(key) || is.empty(config)) {
return '';
}
let string = utils.getDeep(config.i18n, key);
let string = getDeep(config.i18n, key);
if (utils.is.empty(string)) {
if (is.empty(string)) {
return '';
}
@ -25,7 +27,7 @@ const i18n = {
key,
value,
]) => {
string = utils.replaceAll(string, key, value);
string = replaceAll(string, key, value);
});
return string;

View File

@ -4,10 +4,10 @@
import controls from './controls';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
import { off, on, toggleListener, trigger } from './utils/events';
import is from './utils/is';
class Listeners {
constructor(player) {
@ -32,7 +32,7 @@ class Listeners {
// If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) {
if (!is.number(code)) {
return;
}
@ -73,10 +73,10 @@ class Listeners {
// Check focused element
// and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement();
if (utils.is.element(focused) && (
const focused = getFocusElement();
if (is.element(focused) && (
focused !== this.player.elements.inputs.seek &&
utils.matches(focused, this.player.config.selectors.editable))
matches(focused, this.player.config.selectors.editable))
) {
return;
}
@ -195,41 +195,41 @@ class Listeners {
this.player.touch = true;
// Add touch class
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
utils.off(document.body, 'touchstart', this.firstTouch);
off(document.body, 'touchstart', this.firstTouch);
}
// Global window & document listeners
global(toggle = true) {
// Keyboard shortcuts
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
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
toggleListener(document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events
utils.on(document.body, 'touchstart', this.firstTouch);
on(document.body, 'touchstart', this.firstTouch);
}
// Container listeners
container() {
// Keyboard shortcuts
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
// Remove class on blur/focusout
utils.on(this.player.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false);
on(this.player.elements.container, 'focusout', event => {
toggleClass(event.target, this.player.config.classNames.tabFocus, false);
});
// Add classname to tabbed elements
utils.on(this.player.elements.container, 'keydown', event => {
on(this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) {
return;
}
@ -237,12 +237,12 @@ class Listeners {
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true);
toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0);
});
// 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;
// Remove button states for fullscreen
@ -276,20 +276,20 @@ class Listeners {
// Listen for media events
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
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
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
utils.on(this.player.media, 'loadeddata', () => {
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio);
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
on(this.player.media, 'loadeddata canplay', () => {
toggleHidden(this.player.elements.volume, !this.player.hasAudio);
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
});
// Handle the media finishing
utils.on(this.player.media, 'ended', () => {
on(this.player.media, 'ended', () => {
// Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart
@ -298,20 +298,20 @@ class Listeners {
});
// 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
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
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
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
// 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) {
return;
}
@ -326,15 +326,15 @@ class Listeners {
// Click video
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// 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)
if (!utils.is.element(wrapper)) {
if (!is.element(wrapper)) {
return;
}
// On click play, pause ore restart
utils.on(wrapper, 'click', () => {
on(wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return;
@ -353,7 +353,7 @@ class Listeners {
// Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on(
on(
this.player.elements.wrapper,
'contextmenu',
event => {
@ -364,13 +364,13 @@ class Listeners {
}
// Volume change
utils.on(this.player.media, 'volumechange', () => {
on(this.player.media, 'volumechange', () => {
// Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
});
// Speed change
utils.on(this.player.media, 'ratechange', () => {
on(this.player.media, 'ratechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'speed');
@ -379,19 +379,19 @@ class Listeners {
});
// Quality request
utils.on(this.player.media, 'qualityrequested', event => {
on(this.player.media, 'qualityrequested', event => {
// Save to storage
this.player.storage.set({ quality: event.detail.quality });
});
// Quality change
utils.on(this.player.media, 'qualitychange', event => {
on(this.player.media, 'qualitychange', event => {
// Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
});
// Caption language change
utils.on(this.player.media, 'languagechange', () => {
on(this.player.media, 'languagechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
@ -400,7 +400,7 @@ class Listeners {
});
// Captions toggle
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
on(this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
@ -410,7 +410,7 @@ class Listeners {
// Proxy events to container
// 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',
'keydown',
]).join(' '), event => {
@ -421,7 +421,7 @@ class Listeners {
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
const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
const hasCustomHandler = is.function(customHandler);
let returned = true;
// Execute custom handler
@ -442,33 +442,33 @@ class Listeners {
}
// 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);
}
};
// 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 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
on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
bind(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
// Pause
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
// Rewind
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
bind(this.player.elements.buttons.rewind, 'click', this.player.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
on(
bind(
this.player.elements.buttons.mute,
'click',
() => {
@ -478,10 +478,10 @@ class Listeners {
);
// Captions toggle
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
bind(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
// Fullscreen toggle
on(
bind(
this.player.elements.buttons.fullscreen,
'click',
() => {
@ -491,7 +491,7 @@ class Listeners {
);
// Picture-in-Picture
on(
bind(
this.player.elements.buttons.pip,
'click',
() => {
@ -501,15 +501,15 @@ class Listeners {
);
// 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
on(this.player.elements.buttons.settings, 'click', event => {
bind(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event);
});
// Settings menu
on(this.player.elements.settings.form, 'click', event => {
bind(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Go back to home tab on click
@ -519,7 +519,7 @@ class Listeners {
};
// 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(
event,
() => {
@ -528,7 +528,7 @@ class Listeners {
},
'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(
event,
() => {
@ -537,7 +537,7 @@ class Listeners {
},
'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(
event,
() => {
@ -553,14 +553,14 @@ class Listeners {
});
// 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 percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seek-value', percent);
});
// Pause while seeking
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which;
@ -590,7 +590,7 @@ class Listeners {
});
// Seek
on(
bind(
this.player.elements.inputs.seek,
inputEvent,
event => {
@ -599,7 +599,7 @@ class Listeners {
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
if (utils.is.empty(seekTo)) {
if (is.empty(seekTo)) {
seekTo = seek.value;
}
@ -612,8 +612,8 @@ class Listeners {
// Current time invert
// 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)) {
on(this.player.elements.display.currentTime, 'click', () => {
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
bind(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.player.currentTime === 0) {
return;
@ -626,7 +626,7 @@ class Listeners {
}
// Volume
on(
bind(
this.player.elements.inputs.volume,
inputEvent,
event => {
@ -637,21 +637,21 @@ class Listeners {
// Polyfill for lower fill in <input type="range"> for webkit
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);
});
}
// 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)
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';
});
// 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 = [
'mousedown',
'touchstart',
@ -659,11 +659,11 @@ class Listeners {
});
// 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;
// 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
ui.toggleControls.call(this.player, event.type === 'focusin');
@ -672,7 +672,7 @@ class Listeners {
if (event.type === 'focusin') {
// Restore transition
setTimeout(() => {
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0);
// Delay a little more for keyboard users
@ -686,7 +686,7 @@ class Listeners {
});
// Mouse wheel for volume
on(
bind(
this.player.elements.inputs.volume,
'wheel',
event => {

View File

@ -5,7 +5,7 @@
import html5 from './html5';
import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
import utils from './utils';
import { createElement, toggleClass, wrap } from './utils/elements';
const media = {
// Setup media
@ -17,29 +17,29 @@ const media = {
}
// 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
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
// This will require changes if audio embeds are added
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
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = utils.createElement('div', {
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper);
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('div', {
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});

View File

@ -7,7 +7,12 @@
/* global google */
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 {
/**
@ -44,7 +49,7 @@ class Ads {
}
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() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.sdk)
if (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@ -103,7 +107,7 @@ class Ads {
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() {
// Create the container for our advertisements
this.elements.container = utils.createElement('div', {
this.elements.container = createElement('div', {
class: this.player.config.classNames.ads,
});
this.player.elements.container.appendChild(this.elements.container);
@ -184,7 +188,7 @@ class Ads {
}
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}`;
this.elements.container.setAttribute('data-badge-text', label);
};
@ -212,14 +216,14 @@ class Ads {
this.cuePoints = this.manager.getCuePoints();
// 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 => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (utils.is.element(seekElement)) {
if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', {
const cue = createElement('span', {
class: this.player.config.classNames.cues,
});
@ -266,7 +270,7 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
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) {
@ -393,7 +397,7 @@ class Ads {
this.player.on('seeked', () => {
const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) {
if (is.empty(this.cuePoints)) {
return;
}
@ -530,9 +534,9 @@ class Ads {
trigger(event, ...args) {
const handlers = this.events[event];
if (utils.is.array(handlers)) {
if (is.array(handlers)) {
handlers.forEach(handler => {
if (utils.is.function(handler)) {
if (is.function(handler)) {
handler.apply(this, args);
}
});
@ -546,7 +550,7 @@ class Ads {
* @return {Ads}
*/
on(event, callback) {
if (!utils.is.array(this.events[event])) {
if (!is.array(this.events[event])) {
this.events[event] = [];
}
@ -577,7 +581,7 @@ class Ads {
* @param {string} from
*/
clearSafetyTimer(from) {
if (!utils.is.nullOrUndefined(this.safetyTimer)) {
if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer);

View File

@ -5,7 +5,34 @@
import captions from './../captions';
import controls from './../controls';
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)
function assurePlaybackState(play) {
@ -14,22 +41,21 @@ function assurePlaybackState(play) {
}
if (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 = {
setup() {
// 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
vimeo.setAspectRatio.call(this);
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils
.loadScript(this.config.urls.vimeo.sdk)
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@ -44,7 +70,7 @@ const vimeo = {
// Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
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];
this.elements.wrapper.style.paddingBottom = `${padding}%`;
@ -73,34 +99,34 @@ const vimeo = {
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
const params = utils.buildUrlParams(options);
const params = buildUrlParams(options);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
const id = parseId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// 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);
player.media = utils.replaceElement(wrapper, player.media);
player.media = replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
@ -160,7 +186,7 @@ const vimeo = {
// Set seeking state and trigger event
media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking');
trigger.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
@ -187,7 +213,7 @@ const vimeo = {
.setPlaybackRate(input)
.then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
trigger.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
@ -207,7 +233,7 @@ const vimeo = {
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
utils.dispatchEvent.call(player, player.media, 'volumechange');
trigger.call(player, player.media, 'volumechange');
});
},
});
@ -219,11 +245,11 @@ const vimeo = {
return muted;
},
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(() => {
muted = toggle;
utils.dispatchEvent.call(player, player.media, 'volumechange');
trigger.call(player, player.media, 'volumechange');
});
},
});
@ -235,7 +261,7 @@ const vimeo = {
return loop;
},
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(() => {
loop = toggle;
@ -272,7 +298,7 @@ const vimeo = {
player.embed.getVideoWidth(),
player.embed.getVideoHeight(),
]).then(dimensions => {
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
const ratio = getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, ratio);
});
@ -290,13 +316,13 @@ const vimeo = {
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
utils.dispatchEvent.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'timeupdate');
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
trigger.call(player, player.media, 'durationchange');
});
// Get captions
@ -306,7 +332,7 @@ const vimeo = {
});
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);
});
@ -315,11 +341,11 @@ const vimeo = {
player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !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;
// Fix keyboard focus issues
@ -330,7 +356,7 @@ const vimeo = {
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
trigger.call(player, player.media, 'playing');
});
player.embed.on('pause', () => {
@ -340,16 +366,16 @@ const vimeo = {
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
utils.dispatchEvent.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => {
player.media.buffered = data.percent;
utils.dispatchEvent.call(player, player.media, 'progress');
trigger.call(player, player.media, 'progress');
// Check all loaded
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
@ -357,24 +383,24 @@ const vimeo = {
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
trigger.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
trigger.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'ended');
trigger.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
trigger.call(player, player.media, 'error');
});
// Rebuild UI

View File

@ -4,7 +4,24 @@
import controls from './../controls';
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
function mapQualityUnit(input) {
@ -57,11 +74,11 @@ function mapQualityUnit(input) {
}
function mapQualityUnits(levels) {
if (utils.is.empty(levels)) {
if (is.empty(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)
@ -71,24 +88,24 @@ function assurePlaybackState(play) {
}
if (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 = {
setup() {
// 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
youtube.setAspectRatio.call(this);
// 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);
} else {
// 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);
});
@ -115,10 +132,10 @@ const youtube = {
// Try via undocumented API method first
// This method disappears now and then though...
// 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();
if (utils.is.empty(title)) {
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
@ -127,13 +144,12 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = utils.format(this.config.urls.youtube.api, videoId, key);
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
utils
.fetch(url)
fetch(url)
.then(result => {
if (utils.is.object(result)) {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@ -154,7 +170,7 @@ const youtube = {
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@ -162,23 +178,23 @@ const youtube = {
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(source);
const id = utils.generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
const videoId = parseId(source);
const id = generateId(player.provider);
const container = createElement('div', { id });
player.media = replaceElement(container, player.media);
// Set poster image
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// 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) {
// If we've already fired an error, don't do it again
// YouTube fires onError twice
if (utils.is.object(player.media.error)) {
if (is.object(player.media.error)) {
return;
}
@ -250,10 +266,10 @@ const youtube = {
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
trigger.call(player, player.media, 'error');
},
onPlaybackQualityChange() {
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
trigger.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality,
});
},
@ -264,7 +280,7 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
utils.dispatchEvent.call(player, player.media, 'ratechange');
trigger.call(player, player.media, 'ratechange');
},
onReady(event) {
// Get the instance
@ -305,7 +321,7 @@ const youtube = {
// Set seeking state and trigger event
player.media.seeking = true;
utils.dispatchEvent.call(player, player.media, 'seeking');
trigger.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
@ -334,7 +350,7 @@ const youtube = {
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
trigger.call(player, player.media, 'qualityrequested', false, {
quality,
});
},
@ -349,7 +365,7 @@ const youtube = {
set(input) {
volume = input;
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;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : muted;
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
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);
}
utils.dispatchEvent.call(player, player.media, 'timeupdate');
utils.dispatchEvent.call(player, player.media, 'durationchange');
trigger.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
@ -402,7 +418,7 @@ const youtube = {
// Trigger progress only when we actually buffer something
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
@ -413,7 +429,7 @@ const youtube = {
clearInterval(player.timers.buffering);
// Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
trigger.call(player, player.media, 'canplaythrough');
}
}, 200);
@ -435,7 +451,7 @@ const youtube = {
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
trigger.call(player, player.media, 'seeked');
}
// Handle events
@ -448,11 +464,11 @@ const youtube = {
switch (event.data) {
case -1:
// Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress');
trigger.call(player, player.media, 'progress');
break;
@ -465,7 +481,7 @@ const youtube = {
instance.stopVideo();
instance.playVideo();
} else {
utils.dispatchEvent.call(player, player.media, 'ended');
trigger.call(player, player.media, 'ended');
}
break;
@ -477,11 +493,11 @@ const youtube = {
} else {
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
trigger.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
@ -489,7 +505,7 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange');
trigger.call(player, player.media, 'durationchange');
}
// Get quality
@ -511,7 +527,7 @@ const youtube = {
break;
}
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
trigger.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},

View File

@ -6,9 +6,10 @@
// ==========================================================================
import captions from './captions';
import defaults from './config/defaults';
import { getProviderByUrl, providers, types } from './config/types';
import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import media from './media';
@ -16,9 +17,14 @@ import Ads from './plugins/ads';
import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
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
// TODO: Use a WeakMap for private globals
@ -41,18 +47,18 @@ class Plyr {
this.media = target;
// String selector passed
if (utils.is.string(this.media)) {
if (is.string(this.media)) {
this.media = document.querySelectorAll(this.media);
}
// 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
this.media = this.media[0];
}
// Set config
this.config = utils.extend(
this.config = extend(
{},
defaults,
Plyr.defaults,
@ -108,7 +114,7 @@ class Plyr {
this.debug.log('Support', support);
// 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');
return;
}
@ -144,7 +150,6 @@ class Plyr {
// Embed properties
let iframe = null;
let url = null;
let params = null;
// Different setup based on type
switch (type) {
@ -153,10 +158,10 @@ class Plyr {
iframe = this.media.querySelector('iframe');
// <iframe> type
if (utils.is.element(iframe)) {
if (is.element(iframe)) {
// Detect provider
url = iframe.getAttribute('src');
this.provider = utils.getProviderByUrl(url);
url = parseUrl(iframe.getAttribute('src'));
this.provider = getProviderByUrl(url.toString());
// Rework elements
this.elements.container = this.media;
@ -166,24 +171,23 @@ class Plyr {
this.elements.container.className = '';
// Get attributes from URL and set config
params = utils.getUrlParams(url);
if (!utils.is.empty(params)) {
if (!url.searchParams) {
const truthy = [
'1',
'true',
];
if (truthy.includes(params.autoplay)) {
if (truthy.includes(url.searchParams.get('autoplay'))) {
this.config.autoplay = true;
}
if (truthy.includes(params.loop)) {
if (truthy.includes(url.searchParams.get('loop'))) {
this.config.loop.active = true;
}
// TODO: replace fullscreen.iosNative with this playsinline config option
// YouTube requires the playsinline in the URL
if (this.isYouTube) {
this.config.playsinline = truthy.includes(params.playsinline);
this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
} else {
this.config.playsinline = true;
}
@ -197,7 +201,7 @@ class Plyr {
}
// 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');
return;
}
@ -255,9 +259,9 @@ class Plyr {
this.media.plyr = this;
// Wrap media
if (!utils.is.element(this.elements.container)) {
this.elements.container = utils.createElement('div');
utils.wrap(this.media, this.elements.container);
if (!is.element(this.elements.container)) {
this.elements.container = createElement('div');
wrap(this.media, this.elements.container);
}
// Allow focus to be captured
@ -271,7 +275,7 @@ class Plyr {
// Listen for events if debugging
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}`);
});
}
@ -330,7 +334,7 @@ class Plyr {
* Play the media, or play the advertisement (if they are not blocked)
*/
play() {
if (!utils.is.function(this.media.play)) {
if (!is.function(this.media.play)) {
return null;
}
@ -342,7 +346,7 @@ class Plyr {
* Pause the media
*/
pause() {
if (!this.playing || !utils.is.function(this.media.pause)) {
if (!this.playing || !is.function(this.media.pause)) {
return;
}
@ -383,7 +387,7 @@ class Plyr {
*/
togglePlay(input) {
// 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) {
this.play();
@ -399,7 +403,7 @@ class Plyr {
if (this.isHTML5) {
this.pause();
this.restart();
} else if (utils.is.function(this.media.stop)) {
} else if (is.function(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
*/
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
*/
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
const inputIsValid = utils.is.number(input) && input > 0;
const inputIsValid = is.number(input) && input > 0;
// Set
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
@ -461,7 +465,7 @@ class Plyr {
const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) {
if (is.number(buffered)) {
return buffered;
}
@ -505,17 +509,17 @@ class Plyr {
const max = 1;
const min = 0;
if (utils.is.string(volume)) {
if (is.string(volume)) {
volume = Number(volume);
}
// Load volume from storage if no value specified
if (!utils.is.number(volume)) {
if (!is.number(volume)) {
volume = this.storage.get('volume');
}
// Use config if all else fails
if (!utils.is.number(volume)) {
if (!is.number(volume)) {
({ volume } = this.config);
}
@ -535,7 +539,7 @@ class Plyr {
this.media.volume = volume;
// 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;
}
}
@ -553,7 +557,7 @@ class Plyr {
*/
increaseVolume(step) {
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) {
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;
// Load muted state from storage
if (!utils.is.boolean(toggle)) {
if (!is.boolean(toggle)) {
toggle = this.storage.get('muted');
}
// Use config if all else fails
if (!utils.is.boolean(toggle)) {
if (!is.boolean(toggle)) {
toggle = this.config.muted;
}
@ -624,15 +628,15 @@ class Plyr {
set speed(input) {
let speed = null;
if (utils.is.number(input)) {
if (is.number(input)) {
speed = input;
}
if (!utils.is.number(speed)) {
if (!is.number(speed)) {
speed = this.storage.get('speed');
}
if (!utils.is.number(speed)) {
if (!is.number(speed)) {
speed = this.config.speed.selected;
}
@ -671,19 +675,19 @@ class Plyr {
set quality(input) {
let quality = null;
if (!utils.is.empty(input)) {
if (!is.empty(input)) {
quality = Number(input);
}
if (!utils.is.number(quality)) {
if (!is.number(quality)) {
quality = this.storage.get('quality');
}
if (!utils.is.number(quality)) {
if (!is.number(quality)) {
quality = this.config.quality.selected;
}
if (!utils.is.number(quality)) {
if (!is.number(quality)) {
quality = this.config.quality.default;
}
@ -692,9 +696,9 @@ class Plyr {
}
if (!this.options.quality.includes(quality)) {
const closest = utils.closest(this.options.quality, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
quality = closest;
const value = closest(this.options.quality, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
quality = value;
}
// Update config
@ -717,7 +721,7 @@ class Plyr {
* @param {boolean} input - Whether to loop or not
*/
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.media.loop = toggle;
@ -816,7 +820,7 @@ class Plyr {
* @param {boolean} input - Whether to autoplay or not
*/
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;
}
@ -838,18 +842,18 @@ class Plyr {
}
// 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
utils.toggleState(this.elements.buttons.captions, active);
toggleState(this.elements.buttons.captions, active);
// 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
if (active !== this.captions.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
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
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
if (this.supported.ui && !this.isAudio) {
// 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
const force = typeof toggle === 'undefined' ? undefined : !toggle;
// 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
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);
}
// Trigger event on change
if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown';
utils.dispatchEvent.call(this, this.media, eventName);
trigger.call(this, this.media, eventName);
}
return !hiding;
}
@ -966,7 +970,7 @@ class Plyr {
* @param {function} callback - Callback for when event occurs
*/
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
*/
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 (Object.keys(this.elements).length) {
// Remove elements
utils.removeElement(this.elements.buttons.play);
utils.removeElement(this.elements.captions);
utils.removeElement(this.elements.controls);
utils.removeElement(this.elements.wrapper);
removeElement(this.elements.buttons.play);
removeElement(this.elements.captions);
removeElement(this.elements.controls);
removeElement(this.elements.wrapper);
// Clear for GC
this.elements.buttons.play = null;
@ -1014,7 +1018,7 @@ class Plyr {
}
// Callback
if (utils.is.function(callback)) {
if (is.function(callback)) {
callback();
}
} else {
@ -1022,13 +1026,13 @@ class Plyr {
this.listeners.clear();
// Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container);
replaceElement(this.elements.original, this.elements.container);
// Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true);
trigger.call(this, this.elements.original, 'destroyed', true);
// Callback
if (utils.is.function(callback)) {
if (is.function(callback)) {
callback.call(this.elements.original);
}
@ -1067,7 +1071,7 @@ class Plyr {
clearInterval(this.timers.playing);
// 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();
}
@ -1117,7 +1121,7 @@ class Plyr {
* @param {string} [id] - Unique ID
*/
static loadSprite(url, id) {
return utils.loadSprite(url, id);
return loadSprite(url, id);
}
/**
@ -1128,15 +1132,15 @@ class Plyr {
static setup(selector, options = {}) {
let targets = null;
if (utils.is.string(selector)) {
if (is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) {
} else if (is.nodeList(selector)) {
targets = Array.from(selector);
} else if (utils.is.array(selector)) {
targets = selector.filter(utils.is.element);
} else if (is.array(selector)) {
targets = selector.filter(is.element);
}
if (utils.is.empty(targets)) {
if (is.empty(targets)) {
return null;
}
@ -1144,6 +1148,6 @@ class Plyr {
}
}
Plyr.defaults = utils.cloneDeep(defaults);
Plyr.defaults = cloneDeep(defaults);
export default Plyr;

View File

@ -2,23 +2,24 @@
// Plyr source update
// ==========================================================================
import { providers } from './config/types';
import html5 from './html5';
import media from './media';
import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
import { createElement, insertElement, removeElement } from './utils/elements';
import is from './utils/is';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) {
if (utils.is.string(attributes)) {
utils.insertElement(type, this.media, {
if (is.string(attributes)) {
insertElement(type, this.media, {
src: attributes,
});
} else if (utils.is.array(attributes)) {
} else if (is.array(attributes)) {
attributes.forEach(attribute => {
utils.insertElement(type, this.media, attribute);
insertElement(type, this.media, attribute);
});
}
},
@ -26,7 +27,7 @@ const source = {
// Update source
// Sources are not checked for support so be careful
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');
return;
}
@ -42,17 +43,17 @@ const source = {
this.options.quality = [];
// Remove elements
utils.removeElement(this.media);
removeElement(this.media);
this.media = null;
// Reset class name
if (utils.is.element(this.elements.container)) {
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
}
// Set the type and provider
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
this.supported = support.check(this.type, this.provider, this.config.playsinline);
@ -60,16 +61,16 @@ const source = {
// Create new markup
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
this.media = utils.createElement('video');
this.media = createElement('video');
break;
case 'html5:audio':
this.media = utils.createElement('audio');
this.media = createElement('audio');
break;
case 'youtube:video':
case 'vimeo:video':
this.media = utils.createElement('div', {
this.media = createElement('div', {
src: input.sources[0].src,
});
break;
@ -82,7 +83,7 @@ const source = {
this.elements.container.appendChild(this.media);
// Autoplay the new source?
if (utils.is.boolean(input.autoplay)) {
if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay;
}
@ -94,7 +95,7 @@ const source = {
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!utils.is.empty(input.poster)) {
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {

View File

@ -2,7 +2,8 @@
// Plyr storage
// ==========================================================================
import utils from './utils';
import is from './utils/is';
import { extend } from './utils/objects';
class Storage {
constructor(player) {
@ -37,13 +38,13 @@ class Storage {
const store = window.localStorage.getItem(this.key);
if (utils.is.empty(store)) {
if (is.empty(store)) {
return null;
}
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) {
@ -53,7 +54,7 @@ class Storage {
}
// Can only store objectst
if (!utils.is.object(object)) {
if (!is.object(object)) {
return;
}
@ -61,12 +62,12 @@ class Storage {
let storage = this.get();
// Default to empty object
if (utils.is.empty(storage)) {
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
utils.extend(storage, object);
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));

View File

@ -2,7 +2,10 @@
// 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
const support = {
@ -15,7 +18,6 @@ const support = {
check(type, provider, playsinline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
switch (`${provider}:${type}`) {
@ -48,14 +50,11 @@ const support = {
// Picture-in-picture support
// Safari only currently
pip: (() => {
const browser = utils.getBrowser();
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
})(),
pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
// Airplay support
// Safari only currently
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
@ -69,7 +68,7 @@ const support = {
try {
// Bail if no checking function
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
if (!this.isHTML5 || !is.function(media.canPlayType)) {
return false;
}
@ -119,28 +118,6 @@ const support = {
// Check for textTracks support
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
rangeInput: (() => {
const range = document.createElement('input');
@ -153,7 +130,7 @@ const support = {
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support
transitions: utils.transitionEndEvent !== false,
transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/

View File

@ -6,15 +6,16 @@ import captions from './captions';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
import browser from './utils/browser';
import { getElement, toggleClass, toggleState } from './utils/elements';
import { trigger } from './utils/events';
import is from './utils/is';
import loadImage from './utils/loadImage';
const ui = {
addStyleHook() {
utils.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.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
// Toggle native HTML5 media controls
@ -44,7 +45,7 @@ const ui = {
}
// Inject custom controls if not present
if (!utils.is.element(this.elements.controls)) {
if (!is.element(this.elements.controls)) {
// Inject custom controls
controls.inject.call(this);
@ -85,23 +86,23 @@ const ui = {
ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// 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
this.ready = true;
// Ready event at end of execution stack
setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready');
trigger.call(this, this.media, 'ready');
}, 0);
// Set the title
@ -125,7 +126,7 @@ const ui = {
let label = i18n.get('play', this.config);
// 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}`;
// Set container label
@ -133,7 +134,7 @@ const ui = {
}
// 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 => {
button.setAttribute('aria-label', label);
});
@ -142,14 +143,14 @@ const ui = {
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
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;
}
// 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);
iframe.setAttribute('title', format.replace('{title}', title));
@ -158,7 +159,7 @@ const ui = {
// Toggle poster
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)
@ -167,13 +168,12 @@ const ui = {
this.media.setAttribute('poster', poster);
// Bail if element is missing
if (!utils.is.element(this.elements.poster)) {
if (!is.element(this.elements.poster)) {
return Promise.reject();
}
// Load the image, and set poster if successful
const loadPromise = utils.loadImage(poster)
.then(() => {
const loadPromise = loadImage(poster).then(() => {
this.elements.poster.style.backgroundImage = `url('${poster}')`;
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
@ -194,15 +194,15 @@ const ui = {
// Check playing state
checkPlaying(event) {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.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.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// 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
if (utils.is.event(event) && event.type === 'timeupdate') {
if (is.event(event) && event.type === 'timeupdate') {
return;
}
@ -223,7 +223,7 @@ const ui = {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// 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
ui.toggleControls.call(this);

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
src/js/utils/animation.js Normal file
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
src/js/utils/arrays.js Normal file
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
src/js/utils/browser.js Normal file
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
src/js/utils/elements.js Normal file
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
src/js/utils/events.js Normal file
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
src/js/utils/fetch.js Normal file
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
src/js/utils/is.js Normal file
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
src/js/utils/loadImage.js Normal file
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 });
});
}

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,
});
});
}

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
src/js/utils/objects.js Normal file
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
src/js/utils/strings.js Normal file
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
src/js/utils/time.js Normal file
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
src/js/utils/urls.js Normal file
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;
}