forked from vieter-v/vieter
169 lines
5.3 KiB
JavaScript
169 lines
5.3 KiB
JavaScript
|
/**
|
||
|
* 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?
|
||
|
|
||
|
})();
|