/** * energize.js v0.1.0 * * Speeds up click events on mobile devices. * https://github.com/davidcalhoun/energize.js */ (function() { // Sandbox /** * Don't add to non-touch devices, which don't need to be sped up */ if(!('ontouchstart' in window)) return; var lastClick = {}, isThresholdReached, touchstart, touchmove, touchend, click, closest; /** * isThresholdReached * * Compare touchstart with touchend xy coordinates, * and only fire simulated click event if the coordinates * are nearby. (don't want clicking to be confused with a swipe) */ isThresholdReached = function(startXY, xy) { return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5; }; /** * touchstart * * Save xy coordinates when the user starts touching the screen */ touchstart = function(e) { this.startXY = [e.touches[0].clientX, e.touches[0].clientY]; this.threshold = false; }; /** * touchmove * * Check if the user is scrolling past the threshold. * Have to check here because touchend will not always fire * on some tested devices (Kindle Fire?) */ touchmove = function(e) { // NOOP if the threshold has already been reached if(this.threshold) return false; this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]); }; /** * touchend * * If the user didn't scroll past the threshold between * touchstart and touchend, fire a simulated click. * * (This will fire before a native click) */ touchend = function(e) { // Don't fire a click if the user scrolled past the threshold if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) { return; } /** * Create and fire a click event on the target element * https://developer.mozilla.org/en/DOM/event.initMouseEvent */ var touch = e.changedTouches[0], evt = document.createEvent('MouseEvents'); evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); evt.simulated = true; // distinguish from a normal (nonsimulated) click e.target.dispatchEvent(evt); }; /** * click * * Because we've already fired a click event in touchend, * we need to listed for all native click events here * and suppress them as necessary. */ click = function(e) { /** * Prevent ghost clicks by only allowing clicks we created * in the click event we fired (look for e.simulated) */ var time = Date.now(), timeDiff = time - lastClick.time, x = e.clientX, y = e.clientY, xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)], target = closest(e.target, 'A') || e.target, // needed for standalone apps nodeName = target.nodeName, isLink = nodeName === 'A', standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href"); lastClick.time = time; lastClick.x = x; lastClick.y = y; /** * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire), * so we have to add more logic to determine the time of the last click. Not perfect... * * Older, simpler check: if((!e.simulated) || standAlone) */ if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) { e.preventDefault(); e.stopPropagation(); if(!standAlone) return false; } /** * Special logic for standalone web apps * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window */ if(standAlone) { window.location = target.getAttribute("href"); } /** * Add an energize-focus class to the targeted link (mimics :focus behavior) * TODO: test and/or remove? Does this work? */ if(!target || !target.classList) return; target.classList.add("energize-focus"); window.setTimeout(function(){ target.classList.remove("energize-focus"); }, 150); }; /** * closest * @param {HTMLElement} node current node to start searching from. * @param {string} tagName the (uppercase) name of the tag you're looking for. * * Find the closest ancestor tag of a given node. * * Starts at node and goes up the DOM tree looking for a * matching nodeName, continuing until hitting document.body */ closest = function(node, tagName){ var curNode = node; while(curNode !== document.body) { // go up the dom until we find the tag we're after if(!curNode || curNode.nodeName === tagName) { return curNode; } // found curNode = curNode.parentNode; // not found, so keep going up } return null; // not found }; /** * Add all delegated event listeners * * All the events we care about bubble up to document, * so we can take advantage of event delegation. * * Note: no need to wait for DOMContentLoaded here */ document.addEventListener('touchstart', touchstart, false); document.addEventListener('touchmove', touchmove, false); document.addEventListener('touchend', touchend, false); document.addEventListener('click', click, true); // TODO: why does this use capture? })();