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