aboutsummaryrefslogtreecommitdiffstats
path: root/src/js/utils.js
diff options
context:
space:
mode:
authorSam Potts <me@sampotts.me>2017-11-04 14:25:28 +1100
committerSam Potts <me@sampotts.me>2017-11-04 14:25:28 +1100
commit1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f (patch)
tree349313769a5e3d786a51b45b0a5c849dc7e3211d /src/js/utils.js
parent3d50936b47fdd691816843de962d5699c3c8f596 (diff)
downloadplyr-1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f.tar.lz
plyr-1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f.tar.xz
plyr-1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f.zip
ES6-ified
Diffstat (limited to 'src/js/utils.js')
-rw-r--r--src/js/utils.js667
1 files changed, 667 insertions, 0 deletions
diff --git a/src/js/utils.js b/src/js/utils.js
new file mode 100644
index 00000000..e81954f4
--- /dev/null
+++ b/src/js/utils.js
@@ -0,0 +1,667 @@
+// ==========================================================================
+// Plyr utils
+// ==========================================================================
+
+import support from './support';
+
+const utils = {
+ // Check variable types
+ is: {
+ object(input) {
+ return this.getConstructor(input) === Object;
+ },
+ number(input) {
+ return this.getConstructor(input) === Number && !Number.isNaN(input);
+ },
+ string(input) {
+ return this.getConstructor(input) === String;
+ },
+ boolean(input) {
+ return this.getConstructor(input) === Boolean;
+ },
+ function(input) {
+ return this.getConstructor(input) === Function;
+ },
+ array(input) {
+ return !this.undefined(input) && Array.isArray(input);
+ },
+ nodeList(input) {
+ return !this.undefined(input) && input instanceof NodeList;
+ },
+ htmlElement(input) {
+ return !this.undefined(input) && input instanceof HTMLElement;
+ },
+ event(input) {
+ return !this.undefined(input) && input instanceof Event;
+ },
+ cue(input) {
+ return this.instanceOf(input, window.TextTrackCue) || this.instanceOf(input, window.VTTCue);
+ },
+ track(input) {
+ return (
+ !this.undefined(input) && (this.instanceOf(input, window.TextTrack) || typeof input.kind === 'string')
+ );
+ },
+ undefined(input) {
+ return input !== null && typeof input === 'undefined';
+ },
+ empty(input) {
+ return (
+ input === null ||
+ typeof input === 'undefined' ||
+ ((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) ||
+ (this.object(input) && Object.keys(input).length === 0)
+ );
+ },
+ getConstructor(input) {
+ if (input === null || typeof input === 'undefined') {
+ return null;
+ }
+
+ return input.constructor;
+ },
+ instanceOf(input, constructor) {
+ return Boolean(input && constructor && input instanceof constructor);
+ },
+ },
+
+ // 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),
+ };
+ },
+
+ // Load an external script
+ loadScript(url) {
+ // Check script is not already referenced
+ if (document.querySelectorAll(`script[src="${url}"]`).length) {
+ return;
+ }
+
+ const tag = document.createElement('script');
+ tag.src = url;
+
+ const firstScriptTag = document.getElementsByTagName('script')[0];
+ firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+ },
+
+ // Generate a random ID
+ generateId(prefix) {
+ return `${prefix}-${Math.floor(Math.random() * 10000)}`;
+ },
+
+ // Determine if we're in an iframe
+ inFrame() {
+ try {
+ return window.self !== window.top;
+ } catch (e) {
+ return true;
+ }
+ },
+
+ // 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);
+ }
+ });
+ },
+
+ // Remove an element
+ removeElement(element) {
+ if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) {
+ return null;
+ }
+
+ element.parentNode.removeChild(element);
+
+ return element;
+ },
+
+ // Inaert an element after another
+ insertAfter(element, target) {
+ target.parentNode.insertBefore(element, target.nextSibling);
+ },
+
+ // 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.textContent = text;
+ }
+
+ // Return built element
+ return element;
+ },
+
+ // Insert a DocumentFragment
+ insertElement(type, parent, attributes, text) {
+ // Inject the new <element>
+ parent.appendChild(utils.createElement(type, attributes, text));
+ },
+
+ // Remove all child elements
+ emptyElement(element) {
+ let { length } = element.childNodes;
+
+ while (length > 0) {
+ element.removeChild(element.lastChild);
+ length -= 1;
+ }
+ },
+
+ // Set attributes
+ setAttributes(element, attributes) {
+ Object.keys(attributes).forEach(key => {
+ element.setAttribute(key, attributes[key]);
+ });
+ },
+
+ // 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 class on an element
+ toggleClass(element, className, toggle) {
+ if (utils.is.htmlElement(element)) {
+ const contains = element.classList.contains(className);
+
+ element.classList[toggle ? 'add' : 'remove'](className);
+
+ return (toggle && !contains) || (!toggle && contains);
+ }
+
+ return null;
+ },
+
+ // Has class name
+ hasClass(element, className) {
+ return utils.is.htmlElement(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);
+ },
+
+ // Find the UI controls and store references in custom controls
+ // TODO: Allow settings menus with custom controls
+ findElements() {
+ try {
+ this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
+
+ // Buttons
+ this.elements.buttons = {
+ play: utils.getElements.call(this, this.config.selectors.buttons.play),
+ pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
+ restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
+ rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
+ forward: utils.getElement.call(this, this.config.selectors.buttons.forward),
+ mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
+ pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
+ airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
+ settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
+ captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
+ fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
+ };
+
+ // Progress
+ this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
+
+ // Inputs
+ this.elements.inputs = {
+ seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
+ volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
+ };
+
+ // Display
+ this.elements.display = {
+ buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
+ duration: utils.getElement.call(this, this.config.selectors.display.duration),
+ currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
+ };
+
+ // Seek tooltip
+ if (utils.is.htmlElement(this.elements.progress)) {
+ this.elements.display.seekTooltip = this.elements.progress.querySelector(
+ `.${this.config.classNames.tooltip}`
+ );
+ }
+
+ return true;
+ } catch (error) {
+ // Log it
+ this.warn('It looks like there is a problem with your custom controls HTML', error);
+
+ // Restore native video controls
+ this.toggleNativeControls(true);
+
+ return false;
+ }
+ },
+
+ // 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() {
+ const tabbables = utils.getElements.call(this, 'input:not([disabled]), button:not([disabled])');
+ const first = tabbables[0];
+ const last = tabbables[tabbables.length - 1];
+
+ utils.on(
+ this.elements.container,
+ 'keydown',
+ event => {
+ // If it is tab
+ if (event.which === 9 && this.fullscreen.active) {
+ if (event.target === last && !event.shiftKey) {
+ // Move focus to first element that can be tabbed if Shift isn't used
+ event.preventDefault();
+ first.focus();
+ } else if (event.target === first && event.shiftKey) {
+ // Move focus to last element that can be tabbed if Shift is used
+ event.preventDefault();
+ last.focus();
+ }
+ }
+ },
+ false
+ );
+ },
+
+ // Bind along with custom handler
+ proxy(element, eventName, customListener, defaultListener, passive, capture) {
+ utils.on(
+ element,
+ eventName,
+ event => {
+ if (customListener) {
+ customListener.apply(element, [event]);
+ }
+ defaultListener.apply(element, [event]);
+ },
+ passive,
+ capture
+ );
+ },
+
+ // Toggle event listener
+ toggleListener(elements, event, callback, toggle, passive, capture) {
+ // Bail if no elements
+ if (elements === null || utils.is.undefined(elements)) {
+ return;
+ }
+
+ // If a nodelist is passed, call itself on each node
+ if (elements instanceof NodeList) {
+ // 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 capture boolean
+ let options = utils.is.boolean(capture) ? capture : false;
+
+ // If passive events listeners are supported
+ if (support.passiveListeners) {
+ options = {
+ // Whether the listener can be passive (i.e. default never prevented)
+ passive: utils.is.boolean(passive) ? passive : true,
+ // Whether the listener is a capturing listener or not
+ capture: utils.is.boolean(capture) ? capture : false,
+ };
+ }
+
+ // 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, capture) {
+ utils.toggleListener(element, events, callback, true, passive, capture);
+ },
+
+ // Unbind event handler
+ off(element, events, callback, passive, capture) {
+ utils.toggleListener(element, events, callback, false, passive, capture);
+ },
+
+ // Trigger event
+ dispatchEvent(element, type, bubbles, properties) {
+ // Bail if no element
+ if (!element || !type) {
+ return;
+ }
+
+ // Create and dispatch the event
+ const event = new CustomEvent(type, {
+ bubbles: utils.is.boolean(bubbles) ? bubbles : false,
+ detail: Object.assign({}, properties, {
+ plyr: this instanceof Plyr ? this : null,
+ }),
+ });
+
+ // 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(target, state) {
+ // Bail if no target
+ if (!target) {
+ return null;
+ }
+
+ // Get state
+ const newState = utils.is.boolean(state) ? state : !target.getAttribute('aria-pressed');
+
+ // Set the attribute on target
+ target.setAttribute('aria-pressed', newState);
+
+ return newState;
+ },
+
+ // Get percentage
+ getPercentage(current, max) {
+ if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
+ return 0;
+ }
+ return (current / max * 100).toFixed(2);
+ },
+
+ // Deep extend/merge destination object with N more objects
+ // http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
+ // Removed call to arguments.callee (used explicit function name instead)
+ extend(...objects) {
+ const { length } = objects;
+
+ // Bail if nothing to merge
+ if (!length) {
+ return null;
+ }
+
+ // Return first if specified but nothing to merge
+ if (length === 1) {
+ return objects[0];
+ }
+
+ // First object is the destination
+ let destination = Array.prototype.shift.call(objects);
+ if (!utils.is.object(destination)) {
+ destination = {};
+ }
+
+ // Loop through all objects to merge
+ objects.forEach(source => {
+ if (!utils.is.object(source)) {
+ return;
+ }
+
+ Object.keys(source).forEach(property => {
+ if (source[property] && source[property].constructor && source[property].constructor === Object) {
+ destination[property] = destination[property] || {};
+ utils.extend(destination[property], source[property]);
+ } else {
+ destination[property] = source[property];
+ }
+ });
+ });
+
+ return destination;
+ },
+
+ // Parse YouTube ID from URL
+ parseYouTubeId(url) {
+ 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.number(Number(url))) {
+ return url;
+ }
+
+ const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
+ return url.match(regex) ? RegExp.$2 : url;
+ },
+
+ // Convert object to URL parameters
+ buildUrlParameters(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;
+ },
+
+ // Load an SVG sprite
+ loadSprite(url, id) {
+ if (typeof url !== 'string') {
+ return;
+ }
+
+ const prefix = 'cache-';
+ const hasId = typeof id === 'string';
+ let isCached = false;
+
+ function updateSprite(data) {
+ // Inject content
+ this.innerHTML = data;
+
+ // Inject the SVG to the body
+ document.body.insertBefore(this, document.body.childNodes[0]);
+ }
+
+ // Only load once
+ if (!hasId || !document.querySelectorAll(`#${id}`).length) {
+ // Create container
+ const container = document.createElement('div');
+ container.setAttribute('hidden', '');
+
+ if (hasId) {
+ container.setAttribute('id', id);
+ }
+
+ // Check in cache
+ if (support.storage) {
+ const cached = window.localStorage.getItem(prefix + id);
+ isCached = cached !== null;
+
+ if (isCached) {
+ const data = JSON.parse(cached);
+ updateSprite.call(container, data.content);
+ }
+ }
+
+ // ReSharper disable once InconsistentNaming
+ const xhr = new XMLHttpRequest();
+
+ // XHR for Chrome/Firefox/Opera/Safari
+ if ('withCredentials' in xhr) {
+ xhr.open('GET', url, true);
+ } else {
+ return;
+ }
+
+ // Once loaded, inject to container and body
+ xhr.onload = () => {
+ if (support.storage) {
+ window.localStorage.setItem(
+ prefix + id,
+ JSON.stringify({
+ content: xhr.responseText,
+ })
+ );
+ }
+
+ updateSprite.call(container, xhr.responseText);
+ };
+
+ xhr.send();
+ }
+ },
+
+ // Get the transition end event
+ transitionEnd: (() => {
+ 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 typeof type === 'string' ? type : false;
+ })(),
+};
+
+export default utils;