Merge branch 'develop' into a11y-improvements
# Conflicts: # dist/plyr.js # dist/plyr.js.map # dist/plyr.min.js # dist/plyr.min.js.map # dist/plyr.polyfilled.js # dist/plyr.polyfilled.js.map # dist/plyr.polyfilled.min.js # dist/plyr.polyfilled.min.js.map # src/js/controls.js # src/js/fullscreen.js # src/js/plyr.js # src/js/ui.js # src/js/utils.js
This commit is contained in:
30
src/js/utils/animation.js
Normal file
30
src/js/utils/animation.js
Normal file
@ -0,0 +1,30 @@
|
||||
// ==========================================================================
|
||||
// Animation utils
|
||||
// ==========================================================================
|
||||
|
||||
import { toggleHidden } from './elements';
|
||||
import is from './is';
|
||||
|
||||
export const transitionEndEvent = (() => {
|
||||
const element = document.createElement('span');
|
||||
|
||||
const events = {
|
||||
WebkitTransition: 'webkitTransitionEnd',
|
||||
MozTransition: 'transitionend',
|
||||
OTransition: 'oTransitionEnd otransitionend',
|
||||
transition: 'transitionend',
|
||||
};
|
||||
|
||||
const type = Object.keys(events).find(event => element.style[event] !== undefined);
|
||||
|
||||
return is.string(type) ? events[type] : false;
|
||||
})();
|
||||
|
||||
// Force repaint of element
|
||||
export function repaint(element) {
|
||||
setTimeout(() => {
|
||||
toggleHidden(element, true);
|
||||
element.offsetHeight; // eslint-disable-line
|
||||
toggleHidden(element, false);
|
||||
}, 0);
|
||||
}
|
23
src/js/utils/arrays.js
Normal file
23
src/js/utils/arrays.js
Normal file
@ -0,0 +1,23 @@
|
||||
// ==========================================================================
|
||||
// Array utils
|
||||
// ==========================================================================
|
||||
|
||||
import is from './is';
|
||||
|
||||
// Remove duplicates in an array
|
||||
export function dedupe(array) {
|
||||
if (!is.array(array)) {
|
||||
return array;
|
||||
}
|
||||
|
||||
return array.filter((item, index) => array.indexOf(item) === index);
|
||||
}
|
||||
|
||||
// Get the closest value in an array
|
||||
export function closest(array, value) {
|
||||
if (!is.array(array) || !array.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
||||
}
|
13
src/js/utils/browser.js
Normal file
13
src/js/utils/browser.js
Normal file
@ -0,0 +1,13 @@
|
||||
// ==========================================================================
|
||||
// Browser sniffing
|
||||
// Unfortunately, due to mixed support, UA sniffing is required
|
||||
// ==========================================================================
|
||||
|
||||
const browser = {
|
||||
isIE: /* @cc_on!@ */ false || !!document.documentMode,
|
||||
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
|
||||
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
|
||||
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
|
||||
};
|
||||
|
||||
export default browser;
|
285
src/js/utils/elements.js
Normal file
285
src/js/utils/elements.js
Normal file
@ -0,0 +1,285 @@
|
||||
// ==========================================================================
|
||||
// Element utils
|
||||
// ==========================================================================
|
||||
|
||||
import { toggleListener } from './events';
|
||||
import is from './is';
|
||||
|
||||
// Wrap an element
|
||||
export function wrap(elements, wrapper) {
|
||||
// Convert `elements` to an array, if necessary.
|
||||
const targets = elements.length ? elements : [elements];
|
||||
|
||||
// Loops backwards to prevent having to clone the wrapper on the
|
||||
// first element (see `child` below).
|
||||
Array.from(targets)
|
||||
.reverse()
|
||||
.forEach((element, index) => {
|
||||
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
|
||||
|
||||
// Cache the current parent and sibling.
|
||||
const parent = element.parentNode;
|
||||
const sibling = element.nextSibling;
|
||||
|
||||
// Wrap the element (is automatically removed from its current
|
||||
// parent).
|
||||
child.appendChild(element);
|
||||
|
||||
// If the element had a sibling, insert the wrapper before
|
||||
// the sibling to maintain the HTML structure; otherwise, just
|
||||
// append it to the parent.
|
||||
if (sibling) {
|
||||
parent.insertBefore(child, sibling);
|
||||
} else {
|
||||
parent.appendChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set attributes
|
||||
export function setAttributes(element, attributes) {
|
||||
if (!is.element(element) || is.empty(attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume null and undefined attributes should be left out,
|
||||
// Setting them would otherwise convert them to "null" and "undefined"
|
||||
Object.entries(attributes)
|
||||
.filter(([, value]) => !is.nullOrUndefined(value))
|
||||
.forEach(([key, value]) => element.setAttribute(key, value));
|
||||
}
|
||||
|
||||
// Create a DocumentFragment
|
||||
export function createElement(type, attributes, text) {
|
||||
// Create a new <element>
|
||||
const element = document.createElement(type);
|
||||
|
||||
// Set all passed attributes
|
||||
if (is.object(attributes)) {
|
||||
setAttributes(element, attributes);
|
||||
}
|
||||
|
||||
// Add text node
|
||||
if (is.string(text)) {
|
||||
element.innerText = text;
|
||||
}
|
||||
|
||||
// Return built element
|
||||
return element;
|
||||
}
|
||||
|
||||
// Inaert an element after another
|
||||
export function insertAfter(element, target) {
|
||||
target.parentNode.insertBefore(element, target.nextSibling);
|
||||
}
|
||||
|
||||
// Insert a DocumentFragment
|
||||
export function insertElement(type, parent, attributes, text) {
|
||||
// Inject the new <element>
|
||||
parent.appendChild(createElement(type, attributes, text));
|
||||
}
|
||||
|
||||
// Remove element(s)
|
||||
export function removeElement(element) {
|
||||
if (is.nodeList(element) || is.array(element)) {
|
||||
Array.from(element).forEach(removeElement);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is.element(element) || !is.element(element.parentNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
|
||||
// Remove all child elements
|
||||
export function emptyElement(element) {
|
||||
let { length } = element.childNodes;
|
||||
|
||||
while (length > 0) {
|
||||
element.removeChild(element.lastChild);
|
||||
length -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace element
|
||||
export function replaceElement(newChild, oldChild) {
|
||||
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
oldChild.parentNode.replaceChild(newChild, oldChild);
|
||||
|
||||
return newChild;
|
||||
}
|
||||
|
||||
// Get an attribute object from a string selector
|
||||
export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
// For example:
|
||||
// '.test' to { class: 'test' }
|
||||
// '#test' to { id: 'test' }
|
||||
// '[data-test="test"]' to { 'data-test': 'test' }
|
||||
|
||||
if (!is.string(sel) || is.empty(sel)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const attributes = {};
|
||||
const existing = existingAttributes;
|
||||
|
||||
sel.split(',').forEach(s => {
|
||||
// Remove whitespace
|
||||
const selector = s.trim();
|
||||
const className = selector.replace('.', '');
|
||||
const stripped = selector.replace(/[[\]]/g, '');
|
||||
|
||||
// Get the parts and value
|
||||
const parts = stripped.split('=');
|
||||
const key = parts[0];
|
||||
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
|
||||
|
||||
// Get the first character
|
||||
const start = selector.charAt(0);
|
||||
|
||||
switch (start) {
|
||||
case '.':
|
||||
// Add to existing classname
|
||||
if (is.object(existing) && is.string(existing.class)) {
|
||||
existing.class += ` ${className}`;
|
||||
}
|
||||
|
||||
attributes.class = className;
|
||||
break;
|
||||
|
||||
case '#':
|
||||
// ID selector
|
||||
attributes.id = selector.replace('#', '');
|
||||
break;
|
||||
|
||||
case '[':
|
||||
// Attribute selector
|
||||
attributes[key] = value;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
// Toggle hidden
|
||||
export function toggleHidden(element, hidden) {
|
||||
if (!is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hide = hidden;
|
||||
|
||||
if (!is.boolean(hide)) {
|
||||
hide = !element.hasAttribute('hidden');
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
element.setAttribute('hidden', '');
|
||||
} else {
|
||||
element.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||
export function toggleClass(element, className, force) {
|
||||
if (is.element(element)) {
|
||||
let method = 'toggle';
|
||||
if (typeof force !== 'undefined') {
|
||||
method = force ? 'add' : 'remove';
|
||||
}
|
||||
|
||||
element.classList[method](className);
|
||||
return element.classList.contains(className);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Has class name
|
||||
export function hasClass(element, className) {
|
||||
return is.element(element) && element.classList.contains(className);
|
||||
}
|
||||
|
||||
// Element matches selector
|
||||
export function matches(element, selector) {
|
||||
const prototype = { Element };
|
||||
|
||||
function match() {
|
||||
return Array.from(document.querySelectorAll(selector)).includes(this);
|
||||
}
|
||||
|
||||
const matches =
|
||||
prototype.matches ||
|
||||
prototype.webkitMatchesSelector ||
|
||||
prototype.mozMatchesSelector ||
|
||||
prototype.msMatchesSelector ||
|
||||
match;
|
||||
|
||||
return matches.call(element, selector);
|
||||
}
|
||||
|
||||
// Find all elements
|
||||
export function getElements(selector) {
|
||||
return this.elements.container.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
// Find a single element
|
||||
export function getElement(selector) {
|
||||
return this.elements.container.querySelector(selector);
|
||||
}
|
||||
|
||||
// Get the focused element
|
||||
export function getFocusElement() {
|
||||
let focused = document.activeElement;
|
||||
|
||||
if (!focused || focused === document.body) {
|
||||
focused = null;
|
||||
} else {
|
||||
focused = document.querySelector(':focus');
|
||||
}
|
||||
|
||||
return focused;
|
||||
}
|
||||
|
||||
// Trap focus inside container
|
||||
export function trapFocus(element = null, toggle = false) {
|
||||
if (!is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
const trap = event => {
|
||||
// Bail if not tab key or not fullscreen
|
||||
if (event.key !== 'Tab' || event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current focused element
|
||||
const focused = getFocusElement();
|
||||
|
||||
if (focused === last && !event.shiftKey) {
|
||||
// Move focus to first element that can be tabbed if Shift isn't used
|
||||
first.focus();
|
||||
event.preventDefault();
|
||||
} else if (focused === first && event.shiftKey) {
|
||||
// Move focus to last element that can be tabbed if Shift is used
|
||||
last.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
|
||||
}
|
120
src/js/utils/events.js
Normal file
120
src/js/utils/events.js
Normal file
@ -0,0 +1,120 @@
|
||||
// ==========================================================================
|
||||
// Event utils
|
||||
// ==========================================================================
|
||||
|
||||
import is from './is';
|
||||
|
||||
// Check for passive event listener support
|
||||
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
|
||||
// https://www.youtube.com/watch?v=NPM6172J22g
|
||||
const supportsPassiveListeners = (() => {
|
||||
// Test via a getter in the options object to see if the passive property is accessed
|
||||
let supported = false;
|
||||
try {
|
||||
const options = Object.defineProperty({}, 'passive', {
|
||||
get() {
|
||||
supported = true;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
window.addEventListener('test', null, options);
|
||||
window.removeEventListener('test', null, options);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return supported;
|
||||
})();
|
||||
|
||||
// Toggle event listener
|
||||
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
|
||||
// Bail if no element, event, or callback
|
||||
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow multiple events
|
||||
const events = event.split(' ');
|
||||
|
||||
// Build options
|
||||
// Default to just the capture boolean for browsers with no passive listener support
|
||||
let options = capture;
|
||||
|
||||
// If passive events listeners are supported
|
||||
if (supportsPassiveListeners) {
|
||||
options = {
|
||||
// Whether the listener can be passive (i.e. default never prevented)
|
||||
passive,
|
||||
// Whether the listener is a capturing listener or not
|
||||
capture,
|
||||
};
|
||||
}
|
||||
|
||||
// If a single node is passed, bind the event listener
|
||||
events.forEach(type => {
|
||||
if (this && this.eventListeners && toggle) {
|
||||
// Cache event listener
|
||||
this.eventListeners.push({ element, type, callback, options });
|
||||
}
|
||||
|
||||
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
|
||||
});
|
||||
}
|
||||
|
||||
// Bind event handler
|
||||
export function on(element, events = '', callback, passive = true, capture = false) {
|
||||
toggleListener.call(this, element, events, callback, true, passive, capture);
|
||||
}
|
||||
|
||||
// Unbind event handler
|
||||
export function off(element, events = '', callback, passive = true, capture = false) {
|
||||
toggleListener.call(this, element, events, callback, false, passive, capture);
|
||||
}
|
||||
|
||||
// Bind once-only event handler
|
||||
export function once(element, events = '', callback, passive = true, capture = false) {
|
||||
function onceCallback(...args) {
|
||||
off(element, events, onceCallback, passive, capture);
|
||||
callback.apply(this, args);
|
||||
}
|
||||
|
||||
toggleListener.call(this, element, events, onceCallback, true, passive, capture);
|
||||
}
|
||||
|
||||
// Trigger event
|
||||
export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
|
||||
// Bail if no element
|
||||
if (!is.element(element) || is.empty(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and dispatch the event
|
||||
const event = new CustomEvent(type, {
|
||||
bubbles,
|
||||
detail: Object.assign({}, detail, {
|
||||
plyr: this,
|
||||
}),
|
||||
});
|
||||
|
||||
// Dispatch the event
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Unbind all cached event listeners
|
||||
export function unbindListeners() {
|
||||
if (this && this.eventListeners) {
|
||||
this.eventListeners.forEach(item => {
|
||||
const { element, type, callback, options } = item;
|
||||
element.removeEventListener(type, callback, options);
|
||||
});
|
||||
|
||||
this.eventListeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Run method when / if player is ready
|
||||
export function ready() {
|
||||
return new Promise(
|
||||
resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)),
|
||||
).then(() => {});
|
||||
}
|
42
src/js/utils/fetch.js
Normal file
42
src/js/utils/fetch.js
Normal file
@ -0,0 +1,42 @@
|
||||
// ==========================================================================
|
||||
// Fetch wrapper
|
||||
// Using XHR to avoid issues with older browsers
|
||||
// ==========================================================================
|
||||
|
||||
export default function fetch(url, responseType = 'text') {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
// Check for CORS support
|
||||
if (!('withCredentials' in request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
request.addEventListener('load', () => {
|
||||
if (responseType === 'text') {
|
||||
try {
|
||||
resolve(JSON.parse(request.responseText));
|
||||
} catch (e) {
|
||||
resolve(request.responseText);
|
||||
}
|
||||
} else {
|
||||
resolve(request.response);
|
||||
}
|
||||
});
|
||||
|
||||
request.addEventListener('error', () => {
|
||||
throw new Error(request.statusText);
|
||||
});
|
||||
|
||||
request.open('GET', url, true);
|
||||
|
||||
// Set the required response type
|
||||
request.responseType = responseType;
|
||||
|
||||
request.send();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
67
src/js/utils/is.js
Normal file
67
src/js/utils/is.js
Normal file
@ -0,0 +1,67 @@
|
||||
// ==========================================================================
|
||||
// Type checking utils
|
||||
// ==========================================================================
|
||||
|
||||
const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
|
||||
|
||||
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
|
||||
|
||||
const is = {
|
||||
object(input) {
|
||||
return getConstructor(input) === Object;
|
||||
},
|
||||
number(input) {
|
||||
return getConstructor(input) === Number && !Number.isNaN(input);
|
||||
},
|
||||
string(input) {
|
||||
return getConstructor(input) === String;
|
||||
},
|
||||
boolean(input) {
|
||||
return getConstructor(input) === Boolean;
|
||||
},
|
||||
function(input) {
|
||||
return getConstructor(input) === Function;
|
||||
},
|
||||
array(input) {
|
||||
return !is.nullOrUndefined(input) && Array.isArray(input);
|
||||
},
|
||||
weakMap(input) {
|
||||
return instanceOf(input, WeakMap);
|
||||
},
|
||||
nodeList(input) {
|
||||
return instanceOf(input, NodeList);
|
||||
},
|
||||
element(input) {
|
||||
return instanceOf(input, Element);
|
||||
},
|
||||
textNode(input) {
|
||||
return getConstructor(input) === Text;
|
||||
},
|
||||
event(input) {
|
||||
return instanceOf(input, Event);
|
||||
},
|
||||
cue(input) {
|
||||
return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
|
||||
},
|
||||
track(input) {
|
||||
return instanceOf(input, TextTrack) || (!is.nullOrUndefined(input) && is.string(input.kind));
|
||||
},
|
||||
url(input) {
|
||||
return (
|
||||
!is.nullOrUndefined(input) &&
|
||||
/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input)
|
||||
);
|
||||
},
|
||||
nullOrUndefined(input) {
|
||||
return input === null || typeof input === 'undefined';
|
||||
},
|
||||
empty(input) {
|
||||
return (
|
||||
is.nullOrUndefined(input) ||
|
||||
((is.string(input) || is.array(input) || is.nodeList(input)) && !input.length) ||
|
||||
(is.object(input) && !Object.keys(input).length)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default is;
|
19
src/js/utils/loadImage.js
Normal file
19
src/js/utils/loadImage.js
Normal file
@ -0,0 +1,19 @@
|
||||
// ==========================================================================
|
||||
// Load image avoiding xhr/fetch CORS issues
|
||||
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
|
||||
// By default it checks if it is at least 1px, but you can add a second argument to change this
|
||||
// ==========================================================================
|
||||
|
||||
export default function loadImage(src, minWidth = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
|
||||
const handler = () => {
|
||||
delete image.onload;
|
||||
delete image.onerror;
|
||||
(image.naturalWidth >= minWidth ? resolve : reject)(image);
|
||||
};
|
||||
|
||||
Object.assign(image, { onload: handler, onerror: handler, src });
|
||||
});
|
||||
}
|
14
src/js/utils/loadScript.js
Normal file
14
src/js/utils/loadScript.js
Normal file
@ -0,0 +1,14 @@
|
||||
// ==========================================================================
|
||||
// Load an external script
|
||||
// ==========================================================================
|
||||
|
||||
import loadjs from 'loadjs';
|
||||
|
||||
export default function loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
loadjs(url, {
|
||||
success: resolve,
|
||||
error: reject,
|
||||
});
|
||||
});
|
||||
}
|
75
src/js/utils/loadSprite.js
Normal file
75
src/js/utils/loadSprite.js
Normal file
@ -0,0 +1,75 @@
|
||||
// ==========================================================================
|
||||
// Sprite loader
|
||||
// ==========================================================================
|
||||
|
||||
import Storage from './../storage';
|
||||
import is from './is';
|
||||
|
||||
// Load an external SVG sprite
|
||||
export default function loadSprite(url, id) {
|
||||
if (!is.string(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = 'cache';
|
||||
const hasId = is.string(id);
|
||||
let isCached = false;
|
||||
|
||||
const exists = () => document.getElementById(id) !== null;
|
||||
|
||||
const update = (container, data) => {
|
||||
container.innerHTML = data;
|
||||
|
||||
// Check again incase of race condition
|
||||
if (hasId && exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the SVG to the body
|
||||
document.body.insertAdjacentElement('afterbegin', container);
|
||||
};
|
||||
|
||||
// Only load once if ID set
|
||||
if (!hasId || !exists()) {
|
||||
const useStorage = Storage.supported;
|
||||
|
||||
// Create container
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('hidden', '');
|
||||
|
||||
if (hasId) {
|
||||
container.setAttribute('id', id);
|
||||
}
|
||||
|
||||
// Check in cache
|
||||
if (useStorage) {
|
||||
const cached = window.localStorage.getItem(`${prefix}-${id}`);
|
||||
isCached = cached !== null;
|
||||
|
||||
if (isCached) {
|
||||
const data = JSON.parse(cached);
|
||||
update(container, data.content);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the sprite
|
||||
fetch(url)
|
||||
.then(result => {
|
||||
if (is.empty(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (useStorage) {
|
||||
window.localStorage.setItem(
|
||||
`${prefix}-${id}`,
|
||||
JSON.stringify({
|
||||
content: result,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
update(container, result);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
42
src/js/utils/objects.js
Normal file
42
src/js/utils/objects.js
Normal file
@ -0,0 +1,42 @@
|
||||
// ==========================================================================
|
||||
// Object utils
|
||||
// ==========================================================================
|
||||
|
||||
import is from './is';
|
||||
|
||||
// Clone nested objects
|
||||
export function cloneDeep(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
// Get a nested value in an object
|
||||
export function getDeep(object, path) {
|
||||
return path.split('.').reduce((obj, key) => obj && obj[key], object);
|
||||
}
|
||||
|
||||
// Deep extend destination object with N more objects
|
||||
export function extend(target = {}, ...sources) {
|
||||
if (!sources.length) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const source = sources.shift();
|
||||
|
||||
if (!is.object(source)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
Object.keys(source).forEach(key => {
|
||||
if (is.object(source[key])) {
|
||||
if (!Object.keys(target).includes(key)) {
|
||||
Object.assign(target, { [key]: {} });
|
||||
}
|
||||
|
||||
extend(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(target, { [key]: source[key] });
|
||||
}
|
||||
});
|
||||
|
||||
return extend(target, ...sources);
|
||||
}
|
85
src/js/utils/strings.js
Normal file
85
src/js/utils/strings.js
Normal file
@ -0,0 +1,85 @@
|
||||
// ==========================================================================
|
||||
// String utils
|
||||
// ==========================================================================
|
||||
|
||||
import is from './is';
|
||||
|
||||
// Generate a random ID
|
||||
export function generateId(prefix) {
|
||||
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
|
||||
}
|
||||
|
||||
// Format string
|
||||
export function format(input, ...args) {
|
||||
if (is.empty(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
|
||||
}
|
||||
|
||||
// Get percentage
|
||||
export function getPercentage(current, max) {
|
||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (current / max * 100).toFixed(2);
|
||||
}
|
||||
|
||||
// Replace all occurances of a string in a string
|
||||
export function replaceAll(input = '', find = '', replace = '') {
|
||||
return input.replace(
|
||||
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
|
||||
replace.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to title case
|
||||
export function toTitleCase(input = '') {
|
||||
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
|
||||
}
|
||||
|
||||
// Convert string to pascalCase
|
||||
export function toPascalCase(input = '') {
|
||||
let string = input.toString();
|
||||
|
||||
// Convert kebab case
|
||||
string = replaceAll(string, '-', ' ');
|
||||
|
||||
// Convert snake case
|
||||
string = replaceAll(string, '_', ' ');
|
||||
|
||||
// Convert to title case
|
||||
string = toTitleCase(string);
|
||||
|
||||
// Convert to pascal case
|
||||
return replaceAll(string, ' ', '');
|
||||
}
|
||||
|
||||
// Convert string to pascalCase
|
||||
export function toCamelCase(input = '') {
|
||||
let string = input.toString();
|
||||
|
||||
// Convert to pascal case
|
||||
string = toPascalCase(string);
|
||||
|
||||
// Convert first character to lowercase
|
||||
return string.charAt(0).toLowerCase() + string.slice(1);
|
||||
}
|
||||
|
||||
// Remove HTML from a string
|
||||
export function stripHTML(source) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const element = document.createElement('div');
|
||||
fragment.appendChild(element);
|
||||
element.innerHTML = source;
|
||||
return fragment.firstChild.innerText;
|
||||
}
|
||||
|
||||
// Like outerHTML, but also works for DocumentFragment
|
||||
export function getHTML(element) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.appendChild(element);
|
||||
return wrapper.innerHTML;
|
||||
}
|
36
src/js/utils/time.js
Normal file
36
src/js/utils/time.js
Normal file
@ -0,0 +1,36 @@
|
||||
// ==========================================================================
|
||||
// Time utils
|
||||
// ==========================================================================
|
||||
|
||||
import is from './is';
|
||||
|
||||
// Time helpers
|
||||
export const getHours = value => parseInt((value / 60 / 60) % 60, 10);
|
||||
export const getMinutes = value => parseInt((value / 60) % 60, 10);
|
||||
export const getSeconds = value => parseInt(value % 60, 10);
|
||||
|
||||
// Format time to UI friendly string
|
||||
export function formatTime(time = 0, displayHours = false, inverted = false) {
|
||||
// Bail if the value isn't a number
|
||||
if (!is.number(time)) {
|
||||
return formatTime(null, displayHours, inverted);
|
||||
}
|
||||
|
||||
// Format time component to add leading zero
|
||||
const format = value => `0${value}`.slice(-2);
|
||||
|
||||
// Breakdown to hours, mins, secs
|
||||
let hours = getHours(time);
|
||||
const mins = getMinutes(time);
|
||||
const secs = getSeconds(time);
|
||||
|
||||
// Do we need to display hours?
|
||||
if (displayHours || hours > 0) {
|
||||
hours = `${hours}:`;
|
||||
} else {
|
||||
hours = '';
|
||||
}
|
||||
|
||||
// Render
|
||||
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
||||
}
|
39
src/js/utils/urls.js
Normal file
39
src/js/utils/urls.js
Normal file
@ -0,0 +1,39 @@
|
||||
// ==========================================================================
|
||||
// URL utils
|
||||
// ==========================================================================
|
||||
|
||||
import is from './is';
|
||||
|
||||
/**
|
||||
* Parse a string to a URL object
|
||||
* @param {string} input - the URL to be parsed
|
||||
* @param {boolean} safe - failsafe parsing
|
||||
*/
|
||||
export function parseUrl(input, safe = true) {
|
||||
let url = input;
|
||||
|
||||
if (safe) {
|
||||
const parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
url = parser.href;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert object to URLSearchParams
|
||||
export function buildUrlParams(input) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (is.object(input)) {
|
||||
Object.entries(input).forEach(([key, value]) => {
|
||||
params.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
Reference in New Issue
Block a user