From 392dfd024c505f5ae1bbb2f0d3e0793c251a1f35 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 13 Jun 2018 00:02:55 +1000 Subject: Utils broken down into seperate files and exports --- src/js/utils/animation.js | 30 +++++ src/js/utils/arrays.js | 23 ++++ src/js/utils/browser.js | 13 ++ src/js/utils/elements.js | 307 +++++++++++++++++++++++++++++++++++++++++++++ src/js/utils/events.js | 98 +++++++++++++++ src/js/utils/fetch.js | 42 +++++++ src/js/utils/is.js | 64 ++++++++++ src/js/utils/loadImage.js | 19 +++ src/js/utils/loadScript.js | 14 +++ src/js/utils/loadSprite.js | 75 +++++++++++ src/js/utils/objects.js | 42 +++++++ src/js/utils/strings.js | 82 ++++++++++++ src/js/utils/time.js | 36 ++++++ src/js/utils/urls.js | 44 +++++++ 14 files changed, 889 insertions(+) create mode 100644 src/js/utils/animation.js create mode 100644 src/js/utils/arrays.js create mode 100644 src/js/utils/browser.js create mode 100644 src/js/utils/elements.js create mode 100644 src/js/utils/events.js create mode 100644 src/js/utils/fetch.js create mode 100644 src/js/utils/is.js create mode 100644 src/js/utils/loadImage.js create mode 100644 src/js/utils/loadScript.js create mode 100644 src/js/utils/loadSprite.js create mode 100644 src/js/utils/objects.js create mode 100644 src/js/utils/strings.js create mode 100644 src/js/utils/time.js create mode 100644 src/js/utils/urls.js (limited to 'src/js/utils') diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js new file mode 100644 index 00000000..95e39f03 --- /dev/null +++ b/src/js/utils/animation.js @@ -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); +} diff --git a/src/js/utils/arrays.js b/src/js/utils/arrays.js new file mode 100644 index 00000000..69ef242c --- /dev/null +++ b/src/js/utils/arrays.js @@ -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)); +} diff --git a/src/js/utils/browser.js b/src/js/utils/browser.js new file mode 100644 index 00000000..d574f683 --- /dev/null +++ b/src/js/utils/browser.js @@ -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; diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js new file mode 100644 index 00000000..4d4f97cd --- /dev/null +++ b/src/js/utils/elements.js @@ -0,0 +1,307 @@ +// ========================================================================== +// Element utils +// ========================================================================== + +import { off, on } from './events'; +import is from './is'; + +// Wrap an element +export function wrap(elements, wrapper) { + // Convert `elements` to an array, if necessary. + const targets = elements.length ? elements : [elements]; + + // Loops backwards to prevent having to clone the wrapper on the + // first element (see `child` below). + Array.from(targets) + .reverse() + .forEach((element, index) => { + const child = index > 0 ? wrapper.cloneNode(true) : wrapper; + + // Cache the current parent and sibling. + const parent = element.parentNode; + const sibling = element.nextSibling; + + // Wrap the element (is automatically removed from its current + // parent). + child.appendChild(element); + + // If the element had a sibling, insert the wrapper before + // the sibling to maintain the HTML structure; otherwise, just + // append it to the parent. + if (sibling) { + parent.insertBefore(child, sibling); + } else { + parent.appendChild(child); + } + }); +} + +// Set attributes +export function setAttributes(element, attributes) { + if (!is.element(element) || is.empty(attributes)) { + return; + } + + Object.entries(attributes).forEach(([ + key, + value, + ]) => { + element.setAttribute(key, value); + }); +} + +// Create a DocumentFragment +export function createElement(type, attributes, text) { + // Create a new + 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 + parent.appendChild(createElement(type, attributes, text)); +} + +// Remove element(s) +export function removeElement(element) { + if (is.nodeList(element) || is.array(element)) { + Array.from(element).forEach(removeElement); + return; + } + + if (!is.element(element) || !is.element(element.parentNode)) { + return; + } + + element.parentNode.removeChild(element); +} + +// Remove all child elements +export function emptyElement(element) { + let { length } = element.childNodes; + + while (length > 0) { + element.removeChild(element.lastChild); + length -= 1; + } +} + +// Replace element +export function replaceElement(newChild, oldChild) { + if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { + return null; + } + + oldChild.parentNode.replaceChild(newChild, oldChild); + + return newChild; +} + +// Get an attribute object from a string selector +export function getAttributesFromSelector(sel, existingAttributes) { + // For example: + // '.test' to { class: 'test' } + // '#test' to { id: 'test' } + // '[data-test="test"]' to { 'data-test': 'test' } + + if (!is.string(sel) || is.empty(sel)) { + return {}; + } + + const attributes = {}; + const existing = existingAttributes; + + sel.split(',').forEach(s => { + // Remove whitespace + const selector = s.trim(); + const className = selector.replace('.', ''); + const stripped = selector.replace(/[[\]]/g, ''); + + // Get the parts and value + const parts = stripped.split('='); + const key = parts[0]; + const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; + + // Get the first character + const start = selector.charAt(0); + + switch (start) { + case '.': + // Add to existing classname + if (is.object(existing) && is.string(existing.class)) { + existing.class += ` ${className}`; + } + + attributes.class = className; + break; + + case '#': + // ID selector + attributes.id = selector.replace('#', ''); + break; + + case '[': + // Attribute selector + attributes[key] = value; + + break; + + default: + break; + } + }); + + return attributes; +} + +// Toggle hidden +export function toggleHidden(element, hidden) { + if (!is.element(element)) { + return; + } + + let hide = hidden; + + if (!is.boolean(hide)) { + hide = !element.hasAttribute('hidden'); + } + + if (hide) { + element.setAttribute('hidden', ''); + } else { + element.removeAttribute('hidden'); + } +} + +// Mirror Element.classList.toggle, with IE compatibility for "force" argument +export function toggleClass(element, className, force) { + if (is.element(element)) { + let method = 'toggle'; + if (typeof force !== 'undefined') { + method = force ? 'add' : 'remove'; + } + + element.classList[method](className); + return element.classList.contains(className); + } + + return null; +} + +// Has class name +export function hasClass(element, className) { + return is.element(element) && element.classList.contains(className); +} + +// Element matches selector +export function matches(element, selector) { + const prototype = { Element }; + + function match() { + return Array.from(document.querySelectorAll(selector)).includes(this); + } + + const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + + return matches.call(element, selector); +} + +// Find all elements +export function getElements(selector) { + return this.elements.container.querySelectorAll(selector); +} + +// Find a single element +export function getElement(selector) { + return this.elements.container.querySelector(selector); +} + +// Get the focused element +export function getFocusElement() { + let focused = document.activeElement; + + if (!focused || focused === document.body) { + focused = null; + } else { + focused = document.querySelector(':focus'); + } + + return focused; +} + +// Trap focus inside container +export function trapFocus(element = null, toggle = false) { + if (!is.element(element)) { + return; + } + + const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + const trap = event => { + // Bail if not tab key or not fullscreen + if (event.key !== 'Tab' || event.keyCode !== 9) { + return; + } + + // Get the current focused element + const focused = getFocusElement(); + + if (focused === last && !event.shiftKey) { + // Move focus to first element that can be tabbed if Shift isn't used + first.focus(); + event.preventDefault(); + } else if (focused === first && event.shiftKey) { + // Move focus to last element that can be tabbed if Shift is used + last.focus(); + event.preventDefault(); + } + }; + + if (toggle) { + on(this.elements.container, 'keydown', trap, false); + } else { + off(this.elements.container, 'keydown', trap, false); + } +} + +// Toggle aria-pressed state on a toggle button +// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles +export function toggleState(element, input) { + // If multiple elements passed + if (is.array(element) || is.nodeList(element)) { + Array.from(element).forEach(target => toggleState(target, input)); + return; + } + + // Bail if no target + if (!is.element(element)) { + return; + } + + // Get state + const pressed = element.getAttribute('aria-pressed') === 'true'; + const state = is.boolean(input) ? input : !pressed; + + // Set the attribute on target + element.setAttribute('aria-pressed', state); +} diff --git a/src/js/utils/events.js b/src/js/utils/events.js new file mode 100644 index 00000000..cb92a93c --- /dev/null +++ b/src/js/utils/events.js @@ -0,0 +1,98 @@ +// ========================================================================== +// Event utils +// ========================================================================== + +import is from './is'; + +// Check for passive event listener support +// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md +// https://www.youtube.com/watch?v=NPM6172J22g +const supportsPassiveListeners = (() => { + // Test via a getter in the options object to see if the passive property is accessed + let supported = false; + try { + const options = Object.defineProperty({}, 'passive', { + get() { + supported = true; + return null; + }, + }); + window.addEventListener('test', null, options); + window.removeEventListener('test', null, options); + } catch (e) { + // Do nothing + } + + return supported; +})(); + +// Toggle event listener +export function toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) { + // Bail if no elemetns, event, or callback + if (is.empty(elements) || is.empty(event) || !is.function(callback)) { + return; + } + + // If a nodelist is passed, call itself on each node + if (is.nodeList(elements) || is.array(elements)) { + // Create listener for each node + Array.from(elements).forEach(element => { + if (element instanceof Node) { + toggleListener.call(null, element, event, callback, toggle, passive, capture); + } + }); + + return; + } + + // Allow multiple events + const events = event.split(' '); + + // Build options + // Default to just the capture boolean for browsers with no passive listener support + let options = capture; + + // If passive events listeners are supported + if (supportsPassiveListeners) { + options = { + // Whether the listener can be passive (i.e. default never prevented) + passive, + // Whether the listener is a capturing listener or not + capture, + }; + } + + // If a single node is passed, bind the event listener + events.forEach(type => { + elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); + }); +} + +// Bind event handler +export function on(element, events = '', callback, passive = true, capture = false) { + toggleListener(element, events, callback, true, passive, capture); +} + +// Unbind event handler +export function off(element, events = '', callback, passive = true, capture = false) { + toggleListener(element, events, callback, false, passive, capture); +} + +// Trigger event +export function trigger(element, type = '', bubbles = false, detail = {}) { + // Bail if no element + if (!is.element(element) || is.empty(type)) { + return; + } + + // Create and dispatch the event + const event = new CustomEvent(type, { + bubbles, + detail: Object.assign({}, detail, { + plyr: this, + }), + }); + + // Dispatch the event + element.dispatchEvent(event); +} diff --git a/src/js/utils/fetch.js b/src/js/utils/fetch.js new file mode 100644 index 00000000..1e506cd0 --- /dev/null +++ b/src/js/utils/fetch.js @@ -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); + } + }); +} diff --git a/src/js/utils/is.js b/src/js/utils/is.js new file mode 100644 index 00000000..d34d3aed --- /dev/null +++ b/src/js/utils/is.js @@ -0,0 +1,64 @@ +// ========================================================================== +// Type checking utils +// ========================================================================== + +const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null); + +const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); + +const is = { + object(input) { + return getConstructor(input) === Object; + }, + number(input) { + return getConstructor(input) === Number && !Number.isNaN(input); + }, + string(input) { + return getConstructor(input) === String; + }, + boolean(input) { + return getConstructor(input) === Boolean; + }, + function(input) { + return getConstructor(input) === Function; + }, + array(input) { + return !is.nullOrUndefined(input) && Array.isArray(input); + }, + weakMap(input) { + return instanceOf(input, WeakMap); + }, + nodeList(input) { + return instanceOf(input, NodeList); + }, + element(input) { + return instanceOf(input, Element); + }, + textNode(input) { + return getConstructor(input) === Text; + }, + event(input) { + return instanceOf(input, Event); + }, + cue(input) { + return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); + }, + track(input) { + return instanceOf(input, TextTrack) || (!is.nullOrUndefined(input) && is.string(input.kind)); + }, + url(input) { + return !is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); + }, + nullOrUndefined(input) { + return input === null || typeof input === 'undefined'; + }, + empty(input) { + return ( + is.nullOrUndefined(input) || + ((is.string(input) || is.array(input) || is.nodeList(input)) && !input.length) || + (is.object(input) && !Object.keys(input).length) + ); + }, +}; + +export default is; diff --git a/src/js/utils/loadImage.js b/src/js/utils/loadImage.js new file mode 100644 index 00000000..8acd2496 --- /dev/null +++ b/src/js/utils/loadImage.js @@ -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 }); + }); +} diff --git a/src/js/utils/loadScript.js b/src/js/utils/loadScript.js new file mode 100644 index 00000000..81ae36f4 --- /dev/null +++ b/src/js/utils/loadScript.js @@ -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, + }); + }); +} diff --git a/src/js/utils/loadSprite.js b/src/js/utils/loadSprite.js new file mode 100644 index 00000000..dbb00cf2 --- /dev/null +++ b/src/js/utils/loadSprite.js @@ -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(() => {}); + } +} diff --git a/src/js/utils/objects.js b/src/js/utils/objects.js new file mode 100644 index 00000000..225bb459 --- /dev/null +++ b/src/js/utils/objects.js @@ -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); +} diff --git a/src/js/utils/strings.js b/src/js/utils/strings.js new file mode 100644 index 00000000..289aeee5 --- /dev/null +++ b/src/js/utils/strings.js @@ -0,0 +1,82 @@ +// ========================================================================== +// String utils +// ========================================================================== + +import is from './is'; + +// Generate a random ID +export function generateId(prefix) { + return `${prefix}-${Math.floor(Math.random() * 10000)}`; +} + +// Format string +export function format(input, ...args) { + if (is.empty(input)) { + return input; + } + + return input.toString().replace(/{(\d+)}/g, (match, i) => (is.string(args[i]) ? args[i] : '')); +} + +// Get percentage +export function getPercentage(current, max) { + if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { + return 0; + } + + return (current / max * 100).toFixed(2); +} + +// Replace all occurances of a string in a string +export function replaceAll(input = '', find = '', replace = '') { + return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); +} + +// Convert to title case +export function toTitleCase(input = '') { + return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()); +} + +// Convert string to pascalCase +export function toPascalCase(input = '') { + let string = input.toString(); + + // Convert kebab case + string = replaceAll(string, '-', ' '); + + // Convert snake case + string = replaceAll(string, '_', ' '); + + // Convert to title case + string = toTitleCase(string); + + // Convert to pascal case + return replaceAll(string, ' ', ''); +} + +// Convert string to pascalCase +export function toCamelCase(input = '') { + let string = input.toString(); + + // Convert to pascal case + string = toPascalCase(string); + + // Convert first character to lowercase + return string.charAt(0).toLowerCase() + string.slice(1); +} + +// Remove HTML from a string +export function stripHTML(source) { + const fragment = document.createDocumentFragment(); + const element = document.createElement('div'); + fragment.appendChild(element); + element.innerHTML = source; + return fragment.firstChild.innerText; +} + +// Like outerHTML, but also works for DocumentFragment +export function getHTML(element) { + const wrapper = document.createElement('div'); + wrapper.appendChild(element); + return wrapper.innerHTML; +} diff --git a/src/js/utils/time.js b/src/js/utils/time.js new file mode 100644 index 00000000..0c9fce64 --- /dev/null +++ b/src/js/utils/time.js @@ -0,0 +1,36 @@ +// ========================================================================== +// Time utils +// ========================================================================== + +import is from './is'; + +// Time helpers +export const getHours = value => parseInt((value / 60 / 60) % 60, 10); +export const getMinutes = value => parseInt((value / 60) % 60, 10); +export const getSeconds = value => parseInt(value % 60, 10); + +// Format time to UI friendly string +export function formatTime(time = 0, displayHours = false, inverted = false) { + // Bail if the value isn't a number + if (!is.number(time)) { + return formatTime(null, displayHours, inverted); + } + + // Format time component to add leading zero + const format = value => `0${value}`.slice(-2); + + // Breakdown to hours, mins, secs + let hours = getHours(time); + const mins = getMinutes(time); + const secs = getSeconds(time); + + // Do we need to display hours? + if (displayHours || hours > 0) { + hours = `${hours}:`; + } else { + hours = ''; + } + + // Render + return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; +} diff --git a/src/js/utils/urls.js b/src/js/utils/urls.js new file mode 100644 index 00000000..28323a1c --- /dev/null +++ b/src/js/utils/urls.js @@ -0,0 +1,44 @@ +// ========================================================================== +// URL utils +// ========================================================================== + +import is from './is'; + +/** + * Parse a string to a URL object + * @param {string} input - the URL to be parsed + * @param {boolean} safe - failsafe parsing + */ +export function parseUrl(input, safe = true) { + let url = input; + + if (safe) { + const parser = document.createElement('a'); + parser.href = url; + url = parser.href; + } + + try { + return new URL(url); + } catch (e) { + return null; + } +} + +// Convert object to URLSearchParams +export function buildUrlParams(input) { + if (!is.object(input)) { + return ''; + } + + const params = new URLSearchParams(); + + Object.entries(input).forEach(([ + key, + value, + ]) => { + params.set(key, value); + }); + + return params; +} -- cgit v1.2.3 From 0ecf7e385482c7bd0600e6a8de05dce49e934c4d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 13 Jun 2018 00:48:42 +1000 Subject: Force string on format --- src/js/utils/strings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/strings.js b/src/js/utils/strings.js index 289aeee5..8ca14ff8 100644 --- a/src/js/utils/strings.js +++ b/src/js/utils/strings.js @@ -15,7 +15,7 @@ export function format(input, ...args) { return input; } - return input.toString().replace(/{(\d+)}/g, (match, i) => (is.string(args[i]) ? args[i] : '')); + return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString()); } // Get percentage -- cgit v1.2.3 From 5dddf8b0ec5d0c3ad486eef04ec27d3c089d18b3 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 13 Jun 2018 00:56:31 +1000 Subject: Logic cleanup --- src/js/utils/urls.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/urls.js b/src/js/utils/urls.js index 28323a1c..89bd77ff 100644 --- a/src/js/utils/urls.js +++ b/src/js/utils/urls.js @@ -27,18 +27,16 @@ export function parseUrl(input, safe = true) { // Convert object to URLSearchParams export function buildUrlParams(input) { - if (!is.object(input)) { - return ''; - } - const params = new URLSearchParams(); - Object.entries(input).forEach(([ - key, - value, - ]) => { - params.set(key, value); - }); + if (is.object(input)) { + Object.entries(input).forEach(([ + key, + value, + ]) => { + params.set(key, value); + }); + } return params; } -- cgit v1.2.3 From fb48b330cc590d39b44ac508f232c552d97e3925 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 12 Jun 2018 17:41:17 +0200 Subject: typo --- src/js/utils/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index a8e05f54..b91195d9 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -28,7 +28,7 @@ const supportsPassiveListeners = (() => { // Toggle event listener export function toggleListener(elements, event, callback, toggle = false, passive = true, capture = false, once = false) { - // Bail if no elemetns, event, or callback + // Bail if no elements, event, or callback if (is.empty(elements) || is.empty(event) || !is.function(callback)) { return; } -- cgit v1.2.3 From f15c1344b0975e7f692492b775062489ee4b1302 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 12 Jun 2018 19:10:00 +0200 Subject: Removed support for multiple elements in toggleListener --- src/js/utils/events.js | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index b91195d9..3923f291 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -27,21 +27,9 @@ const supportsPassiveListeners = (() => { })(); // Toggle event listener -export function toggleListener(elements, event, callback, toggle = false, passive = true, capture = false, once = false) { - // Bail if no elements, event, or callback - if (is.empty(elements) || is.empty(event) || !is.function(callback)) { - return; - } - - // If a nodelist is passed, call itself on each node - if (is.nodeList(elements) || is.array(elements)) { - // Create listener for each node - Array.from(elements).forEach(element => { - if (element instanceof Node) { - toggleListener.call(null, element, event, callback, toggle, passive, capture); - } - }); - +export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false, once = false) { + // Bail if no element, event, or callback + if (!is.element(element) || is.empty(event) || !is.function(callback)) { return; } @@ -66,10 +54,10 @@ export function toggleListener(elements, event, callback, toggle = false, passiv events.forEach(type => { if (this && this.eventListeners && toggle && !once) { // Cache event listener - this.eventListeners.push({ elements, type, callback, options }); + this.eventListeners.push({ element, type, callback, options }); } - elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); + element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); }); } @@ -116,8 +104,8 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) { export function unbindListeners() { if (this && this.eventListeners) { this.eventListeners.forEach(item => { - const { elements, type, callback, options } = item; - elements.removeEventListener(type, callback, options); + const { element, type, callback, options } = item; + element.removeEventListener(type, callback, options); }); this.eventListeners = []; -- cgit v1.2.3 From 53933dff7eaa8074e57cccc7cab18d5be6c83fc4 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 12 Jun 2018 19:39:26 +0200 Subject: Use toggleListener in trapFocus --- src/js/utils/elements.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 4d4f97cd..55866367 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -2,7 +2,7 @@ // Element utils // ========================================================================== -import { off, on } from './events'; +import { toggleListener } from './events'; import is from './is'; // Wrap an element @@ -277,11 +277,7 @@ export function trapFocus(element = null, toggle = false) { } }; - if (toggle) { - on(this.elements.container, 'keydown', trap, false); - } else { - off(this.elements.container, 'keydown', trap, false); - } + toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } // Toggle aria-pressed state on a toggle button -- cgit v1.2.3 From 927326f715132d56c79de70e9b871594514caa2f Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 12 Jun 2018 19:43:08 +0200 Subject: Also remove 'once' event listeners when destroying (they may still be waiting) --- src/js/utils/events.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 3923f291..d6ffc043 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -27,7 +27,7 @@ const supportsPassiveListeners = (() => { })(); // Toggle event listener -export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false, once = false) { +export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { // Bail if no element, event, or callback if (!is.element(element) || is.empty(event) || !is.function(callback)) { return; @@ -52,7 +52,7 @@ export function toggleListener(element, event, callback, toggle = false, passive // If a single node is passed, bind the event listener events.forEach(type => { - if (this && this.eventListeners && toggle && !once) { + if (this && this.eventListeners && toggle) { // Cache event listener this.eventListeners.push({ element, type, callback, options }); } @@ -78,7 +78,7 @@ export function once(element, events = '', callback, passive = true, capture = f callback.apply(this, args); } - toggleListener(element, events, onceCallback, true, passive, capture, true); + toggleListener.call(this, element, events, onceCallback, true, passive, capture); } // Trigger event -- cgit v1.2.3 From 2fe98f3721c68f663f8439e2ab590a0846052271 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 13 Jun 2018 14:29:55 +0200 Subject: Fix condition in events.toggleListener to allow non-elements --- src/js/utils/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index d6ffc043..1e940c71 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -29,7 +29,7 @@ const supportsPassiveListeners = (() => { // Toggle event listener export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { // Bail if no element, event, or callback - if (!is.element(element) || is.empty(event) || !is.function(callback)) { + if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { return; } -- cgit v1.2.3 From 6bff6b317d6adcd9f94c8d4d8ee225d39f784e0f Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 13 Jun 2018 22:18:57 +0200 Subject: Remove line breaks in arrays --- src/js/utils/elements.js | 5 +---- src/js/utils/urls.js | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 55866367..39b944d2 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -42,10 +42,7 @@ export function setAttributes(element, attributes) { return; } - Object.entries(attributes).forEach(([ - key, - value, - ]) => { + Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, value); }); } diff --git a/src/js/utils/urls.js b/src/js/utils/urls.js index 89bd77ff..3ebe622e 100644 --- a/src/js/utils/urls.js +++ b/src/js/utils/urls.js @@ -30,10 +30,7 @@ export function buildUrlParams(input) { const params = new URLSearchParams(); if (is.object(input)) { - Object.entries(input).forEach(([ - key, - value, - ]) => { + Object.entries(input).forEach(([key, value]) => { params.set(key, value); }); } -- cgit v1.2.3 From f1c4752036f58e01df95c30cc9cba4156a0737cd Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 15 Jun 2018 22:52:19 +0200 Subject: Filter out null / undefined in elements.setAttributes --- src/js/utils/elements.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 39b944d2..2d314ed8 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -42,9 +42,11 @@ export function setAttributes(element, attributes) { return; } - Object.entries(attributes).forEach(([key, value]) => { - element.setAttribute(key, value); - }); + // 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 -- cgit v1.2.3 From 2af60c5c0db42655b9adb14a518fc94c2c0f6222 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 15 Jun 2018 23:01:33 +0200 Subject: Add 'ready' promise --- src/js/utils/events.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 1e940c71..9009d1cc 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -111,3 +111,9 @@ export function unbindListeners() { 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(() => {}); +} -- cgit v1.2.3 From d4abb4b1438cb316aacae480e7b7e9b055a60b24 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 17 Jun 2018 01:04:55 +1000 Subject: 120 line width, package upgrade --- src/js/utils/elements.js | 7 ++++++- src/js/utils/events.js | 7 ++++--- src/js/utils/is.js | 5 ++++- src/js/utils/strings.js | 5 ++++- 4 files changed, 18 insertions(+), 6 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 2d314ed8..19e98f6f 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -218,7 +218,12 @@ export function matches(element, selector) { return Array.from(document.querySelectorAll(selector)).includes(this); } - const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + const matches = + prototype.matches || + prototype.webkitMatchesSelector || + prototype.mozMatchesSelector || + prototype.msMatchesSelector || + match; return matches.call(element, selector); } diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9009d1cc..9f734f04 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -113,7 +113,8 @@ export function unbindListeners() { } // 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(() => {}); +export function ready() { + return new Promise( + resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)), + ).then(() => {}); } diff --git a/src/js/utils/is.js b/src/js/utils/is.js index d34d3aed..cb2c07c6 100644 --- a/src/js/utils/is.js +++ b/src/js/utils/is.js @@ -47,7 +47,10 @@ const is = { 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); + 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'; diff --git a/src/js/utils/strings.js b/src/js/utils/strings.js index 8ca14ff8..c872498c 100644 --- a/src/js/utils/strings.js +++ b/src/js/utils/strings.js @@ -29,7 +29,10 @@ export function getPercentage(current, max) { // 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()); + return input.replace( + new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), + replace.toString(), + ); } // Convert to title case -- cgit v1.2.3 From ffd864ed39340c081adb9e4a45b3c9cfe4c139e3 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 18 Jun 2018 21:39:47 +1000 Subject: Work on controls --- src/js/utils/elements.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 69e4d46c..7b58c9ff 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -70,12 +70,19 @@ export function createElement(type, attributes, text) { // Inaert an element after another export function insertAfter(element, target) { + if (!is.element(element) || !is.element(target)) { + return; + } + target.parentNode.insertBefore(element, target.nextSibling); } // Insert a DocumentFragment export function insertElement(type, parent, attributes, text) { - // Inject the new + if (!is.element(parent)) { + return; + } + parent.appendChild(createElement(type, attributes, text)); } @@ -95,6 +102,10 @@ export function removeElement(element) { // Remove all child elements export function emptyElement(element) { + if (!is.element(element)) { + return; + } + let { length } = element.childNodes; while (length > 0) { -- cgit v1.2.3 From 8584f6a1db0eecff8ac2c8c332a837e8a309fbf9 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 18 Jun 2018 22:01:56 +1000 Subject: v3.3.14 --- src/js/utils/fetch.js | 2 +- src/js/utils/loadSprite.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/fetch.js b/src/js/utils/fetch.js index 1e506cd0..ee33ea7c 100644 --- a/src/js/utils/fetch.js +++ b/src/js/utils/fetch.js @@ -26,7 +26,7 @@ export default function fetch(url, responseType = 'text') { }); request.addEventListener('error', () => { - throw new Error(request.statusText); + throw new Error(request.status); }); request.open('GET', url, true); diff --git a/src/js/utils/loadSprite.js b/src/js/utils/loadSprite.js index dbb00cf2..f05795f8 100644 --- a/src/js/utils/loadSprite.js +++ b/src/js/utils/loadSprite.js @@ -3,6 +3,7 @@ // ========================================================================== import Storage from './../storage'; +import fetch from './fetch'; import is from './is'; // Load an external SVG sprite -- cgit v1.2.3 From e04b90c9c030bf5629e034f616a636245770a8d1 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 21 Jun 2018 09:06:28 +1000 Subject: Ads only on HTML5 and .is cleanup --- src/js/utils/is.js | 112 ++++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 58 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/is.js b/src/js/utils/is.js index cb2c07c6..b4760da4 100644 --- a/src/js/utils/is.js +++ b/src/js/utils/is.js @@ -3,65 +3,61 @@ // ========================================================================== const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null); - const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); +const isNullOrUndefined = input => input === null || typeof input === 'undefined'; +const isObject = input => getConstructor(input) === Object; +const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input); +const isString = input => getConstructor(input) === String; +const isBoolean = input => getConstructor(input) === Boolean; +const isFunction = input => getConstructor(input) === Function; +const isArray = input => Array.isArray(input); +const isWeakMap = input => instanceOf(input, WeakMap); +const isNodeList = input => instanceOf(input, NodeList); +const isElement = input => instanceOf(input, Element); +const isTextNode = input => getConstructor(input) === Text; +const isEvent = input => instanceOf(input, Event); +const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); +const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); + +const isEmpty = input => + isNullOrUndefined(input) || + ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) || + (isObject(input) && !Object.keys(input).length); + +const isUrl = input => { + // Accept a URL object + if (instanceOf(input, window.URL)) { + return true; + } -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) - ); - }, + // Add the protocol if required + let string = input; + if (!input.startsWith('http://') || !input.startsWith('https://')) { + string = `http://${input}`; + } + + try { + return !isEmpty(new URL(string).hostname); + } catch (e) { + return false; + } }; -export default is; +export default { + nullOrUndefined: isNullOrUndefined, + object: isObject, + number: isNumber, + string: isString, + boolean: isBoolean, + function: isFunction, + array: isArray, + weakMap: isWeakMap, + nodeList: isNodeList, + element: isElement, + textNode: isTextNode, + event: isEvent, + cue: isCue, + track: isTrack, + url: isUrl, + empty: isEmpty, +}; -- cgit v1.2.3 From 3bf1c59bd6a5beb32ee76ba46e37692c2b1c077f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 28 Jun 2018 23:44:07 +1000 Subject: Work on key bindings for menu --- src/js/utils/elements.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 7b58c9ff..e7e17041 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -294,3 +294,18 @@ export function trapFocus(element = null, toggle = false) { toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } + +// Set focus and tab focus class +export function setFocus(element = null, tabFocus = false) { + if (!is.element(element)) { + return; + } + + // Set regular focus + element.focus(); + + // If we want to mimic keyboard focus via tab + if (tabFocus) { + toggleClass(element, this.config.classNames.tabFocus); + } +} -- cgit v1.2.3 From c99607c85a293a565aa4110a69d04a9e8e9450b6 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 29 Jun 2018 00:21:22 +1000 Subject: Linting, housekeeping, duration fix (fixes #1074) --- src/js/utils/loadSprite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/loadSprite.js b/src/js/utils/loadSprite.js index f05795f8..917bd6ac 100644 --- a/src/js/utils/loadSprite.js +++ b/src/js/utils/loadSprite.js @@ -2,7 +2,7 @@ // Sprite loader // ========================================================================== -import Storage from './../storage'; +import Storage from '../storage'; import fetch from './fetch'; import is from './is'; -- cgit v1.2.3 From e63ad7c74bd763043344fd2bd568a64e7ea18622 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 15 Jul 2018 19:23:28 +1000 Subject: Keyboard and focus improvements --- src/js/utils/elements.js | 41 +++++++++++++----------- src/js/utils/events.js | 81 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 28 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index e7e17041..3a3dfcfd 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -116,7 +116,11 @@ export function emptyElement(element) { // Replace element export function replaceElement(newChild, oldChild) { - if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { + if ( + !is.element(oldChild) || + !is.element(oldChild.parentNode) || + !is.element(newChild) + ) { return null; } @@ -203,6 +207,10 @@ export function toggleHidden(element, hidden) { // Mirror Element.classList.toggle, with IE compatibility for "force" argument export function toggleClass(element, className, force) { + if (is.nodeList(element)) { + return Array.from(element).map(e => toggleClass(e, className, force)); + } + if (is.element(element)) { let method = 'toggle'; if (typeof force !== 'undefined') { @@ -213,7 +221,7 @@ export function toggleClass(element, className, force) { return element.classList.contains(className); } - return null; + return false; } // Has class name @@ -249,26 +257,16 @@ 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 focusable = getElements.call( + this, + 'button:not(:disabled), input:not(:disabled), [tabindex]', + ); const first = focusable[0]; const last = focusable[focusable.length - 1]; @@ -279,7 +277,7 @@ export function trapFocus(element = null, toggle = false) { } // Get the current focused element - const focused = getFocusElement(); + const focused = document.activeElement; if (focused === last && !event.shiftKey) { // Move focus to first element that can be tabbed if Shift isn't used @@ -292,7 +290,14 @@ export function trapFocus(element = null, toggle = false) { } }; - toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); + toggleListener.call( + this, + this.elements.container, + 'keydown', + trap, + toggle, + false, + ); } // Set focus and tab focus class diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9f734f04..9820fcae 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -27,9 +27,21 @@ const supportsPassiveListeners = (() => { })(); // Toggle event listener -export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { +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)) { + if ( + !element || + !('addEventListener' in element) || + is.empty(event) || + !is.function(callback) + ) { return; } @@ -57,28 +69,74 @@ export function toggleListener(element, event, callback, toggle = false, passive this.eventListeners.push({ element, type, callback, options }); } - element[toggle ? 'addEventListener' : 'removeEventListener'](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); +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); +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) { +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); + toggleListener.call( + this, + element, + events, + onceCallback, + true, + passive, + capture, + ); } // Trigger event @@ -115,6 +173,9 @@ export function unbindListeners() { // 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)), + resolve => + this.ready + ? setTimeout(resolve, 0) + : on.call(this, this.elements.container, 'ready', resolve), ).then(() => {}); } -- cgit v1.2.3 From 599b33e55fb4c0aec78cd0895bcd13f3fed12ad2 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 30 Jul 2018 01:13:12 +1000 Subject: Click to play fix, poster fix, iOS controls fixes --- src/js/utils/animation.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index 95e39f03..49bc0b8c 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -15,7 +15,9 @@ export const transitionEndEvent = (() => { transition: 'transitionend', }; - const type = Object.keys(events).find(event => element.style[event] !== undefined); + const type = Object.keys(events).find( + event => element.style[event] !== undefined, + ); return is.string(type) ? events[type] : false; })(); @@ -23,8 +25,12 @@ export const transitionEndEvent = (() => { // Force repaint of element export function repaint(element) { setTimeout(() => { - toggleHidden(element, true); - element.offsetHeight; // eslint-disable-line - toggleHidden(element, false); + try { + toggleHidden(element, true); + element.offsetHeight; // eslint-disable-line + toggleHidden(element, false); + } catch (e) { + // Do nothing + } }, 0); } -- cgit v1.2.3 From 2371619486dbbbfdb0350923684e53963141a7af Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 00:56:44 +1000 Subject: Linting --- src/js/utils/events.js | 81 +++++++------------------------------------------- 1 file changed, 10 insertions(+), 71 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9820fcae..9f734f04 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -27,21 +27,9 @@ const supportsPassiveListeners = (() => { })(); // Toggle event listener -export function toggleListener( - element, - event, - callback, - toggle = false, - passive = true, - capture = false, -) { +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) - ) { + if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { return; } @@ -69,74 +57,28 @@ export function toggleListener( this.eventListeners.push({ element, type, callback, options }); } - element[toggle ? 'addEventListener' : 'removeEventListener']( - 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, - ); +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, - ); +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, -) { +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, - ); + toggleListener.call(this, element, events, onceCallback, true, passive, capture); } // Trigger event @@ -173,9 +115,6 @@ export function unbindListeners() { // 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), + resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)), ).then(() => {}); } -- cgit v1.2.3 From c8db1e55ddff51a1eb4ff08887cbed134116cd88 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 01:26:15 +1000 Subject: Escape closes menu --- src/js/utils/is.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/js/utils') diff --git a/src/js/utils/is.js b/src/js/utils/is.js index b4760da4..2952d486 100644 --- a/src/js/utils/is.js +++ b/src/js/utils/is.js @@ -16,6 +16,7 @@ const isNodeList = input => instanceOf(input, NodeList); const isElement = input => instanceOf(input, Element); const isTextNode = input => getConstructor(input) === Text; const isEvent = input => instanceOf(input, Event); +const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); @@ -56,6 +57,7 @@ export default { element: isElement, textNode: isTextNode, event: isEvent, + keyboardEvent: isKeyboardEvent, cue: isCue, track: isTrack, url: isUrl, -- cgit v1.2.3 From 0bc6b1f1b3a55d28718995cd3bc257ea56d2122c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 5 Aug 2018 22:41:21 +1000 Subject: =?UTF-8?q?Fix=20issue=20where=20enter=20key=20wasn=E2=80=99t=20se?= =?UTF-8?q?tting=20focus=20correctly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/utils/elements.js | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 3a3dfcfd..a6722da2 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -116,11 +116,7 @@ export function emptyElement(element) { // Replace element export function replaceElement(newChild, oldChild) { - if ( - !is.element(oldChild) || - !is.element(oldChild.parentNode) || - !is.element(newChild) - ) { + if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { return null; } @@ -195,7 +191,7 @@ export function toggleHidden(element, hidden) { let hide = hidden; if (!is.boolean(hide)) { - hide = !element.hasAttribute('hidden'); + hide = !element.hidden; } if (hide) { @@ -263,10 +259,7 @@ export function trapFocus(element = null, toggle = false) { return; } - const focusable = getElements.call( - this, - 'button:not(:disabled), input:not(:disabled), [tabindex]', - ); + const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); const first = focusable[0]; const last = focusable[focusable.length - 1]; @@ -290,14 +283,7 @@ export function trapFocus(element = null, toggle = false) { } }; - toggleListener.call( - this, - this.elements.container, - 'keydown', - trap, - toggle, - false, - ); + toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } // Set focus and tab focus class -- cgit v1.2.3 From 297f297d181b694446e04f653da694660e9971b4 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 21:39:16 +1000 Subject: Moved i18n to utils --- src/js/utils/i18n.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/js/utils/i18n.js (limited to 'src/js/utils') diff --git a/src/js/utils/i18n.js b/src/js/utils/i18n.js new file mode 100644 index 00000000..f71e1a42 --- /dev/null +++ b/src/js/utils/i18n.js @@ -0,0 +1,34 @@ +// ========================================================================== +// Plyr internationalization +// ========================================================================== + +import is from './is'; +import { getDeep } from './objects'; +import { replaceAll } from './strings'; + +const i18n = { + get(key = '', config = {}) { + if (is.empty(key) || is.empty(config)) { + return ''; + } + + let string = getDeep(config.i18n, key); + + if (is.empty(string)) { + return ''; + } + + const replace = { + '{seektime}': config.seekTime, + '{title}': config.title, + }; + + Object.entries(replace).forEach(([key, value]) => { + string = replaceAll(string, key, value); + }); + + return string; + }, +}; + +export default i18n; -- cgit v1.2.3 From 87072cb690002a6da71f77030d3d7f41c2edc050 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Tue, 25 Sep 2018 23:29:43 +1000 Subject: Clean up --- src/js/utils/animation.js | 4 +--- src/js/utils/strings.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index 49bc0b8c..6b950b61 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -15,9 +15,7 @@ export const transitionEndEvent = (() => { transition: 'transitionend', }; - const type = Object.keys(events).find( - event => element.style[event] !== undefined, - ); + const type = Object.keys(events).find(event => element.style[event] !== undefined); return is.string(type) ? events[type] : false; })(); diff --git a/src/js/utils/strings.js b/src/js/utils/strings.js index c872498c..6b9a65a2 100644 --- a/src/js/utils/strings.js +++ b/src/js/utils/strings.js @@ -24,7 +24,7 @@ export function getPercentage(current, max) { return 0; } - return (current / max * 100).toFixed(2); + return ((current / max) * 100).toFixed(2); } // Replace all occurances of a string in a string -- cgit v1.2.3 From 515ae32160c8317cfddff07362d9aa399452d05d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 28 Sep 2018 00:30:27 +1000 Subject: Moved hardcoded resources to i18n --- src/js/utils/i18n.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src/js/utils') diff --git a/src/js/utils/i18n.js b/src/js/utils/i18n.js index f71e1a42..758ed695 100644 --- a/src/js/utils/i18n.js +++ b/src/js/utils/i18n.js @@ -6,6 +6,15 @@ import is from './is'; import { getDeep } from './objects'; import { replaceAll } from './strings'; +// Skip i18n for abbreviations and brand names +const resources = { + pip: 'PIP', + airplay: 'AirPlay', + html5: 'HTML5', + vimeo: 'Vimeo', + youtube: 'YouTube', +}; + const i18n = { get(key = '', config = {}) { if (is.empty(key) || is.empty(config)) { @@ -15,6 +24,10 @@ const i18n = { let string = getDeep(config.i18n, key); if (is.empty(string)) { + if (Object.keys(resources).includes(key)) { + return resources[key]; + } + return ''; } -- cgit v1.2.3 From 03c9b53232aeab78a7c592e1bcf387312f77a569 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 24 Oct 2018 22:31:35 +1100 Subject: Allow custom download URL (for streaming, etc) --- src/js/utils/is.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/js/utils') diff --git a/src/js/utils/is.js b/src/js/utils/is.js index 2952d486..ab28f2ab 100644 --- a/src/js/utils/is.js +++ b/src/js/utils/is.js @@ -31,6 +31,11 @@ const isUrl = input => { return true; } + // Must be string from here + if (!isString(input)) { + return false; + } + // Add the protocol if required let string = input; if (!input.startsWith('http://') || !input.startsWith('https://')) { -- cgit v1.2.3 From e8da4326b6947fe78968148a7cdc56a276182202 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 3 Nov 2018 21:17:46 +1100 Subject: Prevent scroll on focus --- src/js/utils/elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js/utils') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index a6722da2..6be634e5 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -293,7 +293,7 @@ export function setFocus(element = null, tabFocus = false) { } // Set regular focus - element.focus(); + element.focus({ preventScroll: true }); // If we want to mimic keyboard focus via tab if (tabFocus) { -- cgit v1.2.3 From 6d9d315ca741f5472902c0f961591fdff9bbf946 Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Thu, 20 Dec 2018 13:05:14 -0500 Subject: fix: Use Math.trunc instead of parseInt --- src/js/utils/time.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/time.js b/src/js/utils/time.js index 7c9860fd..2deccf65 100644 --- a/src/js/utils/time.js +++ b/src/js/utils/time.js @@ -5,9 +5,9 @@ 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); +export const getHours = value => Math.trunc((value / 60 / 60) % 60, 10); +export const getMinutes = value => Math.trunc((value / 60) % 60, 10); +export const getSeconds = value => Math.trunc(value % 60, 10); // Format time to UI friendly string export function formatTime(time = 0, displayHours = false, inverted = false) { -- cgit v1.2.3 From 6782737009bec028b393dbfb8c9897cd0c6df48f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 14 Jan 2019 00:33:48 +1100 Subject: Fullscreen fixes --- src/js/utils/browser.js | 1 + src/js/utils/style.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/js/utils/style.js (limited to 'src/js/utils') diff --git a/src/js/utils/browser.js b/src/js/utils/browser.js index d574f683..11705074 100644 --- a/src/js/utils/browser.js +++ b/src/js/utils/browser.js @@ -5,6 +5,7 @@ const browser = { isIE: /* @cc_on!@ */ false || !!document.documentMode, + isEdge: window.navigator.userAgent.includes('Edge'), 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), diff --git a/src/js/utils/style.js b/src/js/utils/style.js new file mode 100644 index 00000000..a8eb393b --- /dev/null +++ b/src/js/utils/style.js @@ -0,0 +1,40 @@ +// ========================================================================== +// Style utils +// ========================================================================== + +import is from './is'; + +/* function reduceAspectRatio(width, height) { + const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); + const ratio = getRatio(width, height); + return `${width / ratio}:${height / ratio}`; +} */ + +// Set aspect ratio for responsive container +export function setAspectRatio(input) { + let ratio = input; + + if (!is.string(ratio) && !is.nullOrUndefined(this.embed)) { + ({ ratio } = this.embed); + } + + if (!is.string(ratio)) { + ({ ratio } = this.config); + } + + const [x, y] = ratio.split(':').map(Number); + const padding = (100 / x) * y; + + this.elements.wrapper.style.paddingBottom = `${padding}%`; + + // For Vimeo we have an extra
to hide the standard controls and UI + if (this.isVimeo && this.supported.ui) { + const height = 240; + const offset = (height - padding) / (height / 50); + this.media.style.transform = `translateY(-${offset}%)`; + } + + return { padding, ratio }; +} + +export default { setAspectRatio }; -- cgit v1.2.3 From 1d51b287014697701b78c883f70c9963f4253d3c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 26 Jan 2019 22:45:47 +1100 Subject: Tweaks --- src/js/utils/events.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/js/utils') diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9f734f04..d304c312 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -73,10 +73,10 @@ export function off(element, events = '', callback, passive = true, capture = fa // Bind once-only event handler export function once(element, events = '', callback, passive = true, capture = false) { - function onceCallback(...args) { + const onceCallback = (...args) => { off(element, events, onceCallback, passive, capture); callback.apply(this, args); - } + }; toggleListener.call(this, element, events, onceCallback, true, passive, capture); } @@ -114,7 +114,7 @@ export function unbindListeners() { // 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)), + return new Promise(resolve => + this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve), ).then(() => {}); } -- cgit v1.2.3