876 lines
26 KiB
JavaScript
876 lines
26 KiB
JavaScript
// ==========================================================================
|
|
// 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;
|