_energize.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /**
  2. * energize.js v0.1.0
  3. *
  4. * Speeds up click events on mobile devices.
  5. * https://github.com/davidcalhoun/energize.js
  6. */
  7. (function() { // Sandbox
  8. /**
  9. * Don't add to non-touch devices, which don't need to be sped up
  10. */
  11. if(!('ontouchstart' in window)) return;
  12. var lastClick = {},
  13. isThresholdReached, touchstart, touchmove, touchend,
  14. click, closest;
  15. /**
  16. * isThresholdReached
  17. *
  18. * Compare touchstart with touchend xy coordinates,
  19. * and only fire simulated click event if the coordinates
  20. * are nearby. (don't want clicking to be confused with a swipe)
  21. */
  22. isThresholdReached = function(startXY, xy) {
  23. return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5;
  24. };
  25. /**
  26. * touchstart
  27. *
  28. * Save xy coordinates when the user starts touching the screen
  29. */
  30. touchstart = function(e) {
  31. this.startXY = [e.touches[0].clientX, e.touches[0].clientY];
  32. this.threshold = false;
  33. };
  34. /**
  35. * touchmove
  36. *
  37. * Check if the user is scrolling past the threshold.
  38. * Have to check here because touchend will not always fire
  39. * on some tested devices (Kindle Fire?)
  40. */
  41. touchmove = function(e) {
  42. // NOOP if the threshold has already been reached
  43. if(this.threshold) return false;
  44. this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]);
  45. };
  46. /**
  47. * touchend
  48. *
  49. * If the user didn't scroll past the threshold between
  50. * touchstart and touchend, fire a simulated click.
  51. *
  52. * (This will fire before a native click)
  53. */
  54. touchend = function(e) {
  55. // Don't fire a click if the user scrolled past the threshold
  56. if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) {
  57. return;
  58. }
  59. /**
  60. * Create and fire a click event on the target element
  61. * https://developer.mozilla.org/en/DOM/event.initMouseEvent
  62. */
  63. var touch = e.changedTouches[0],
  64. evt = document.createEvent('MouseEvents');
  65. evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
  66. evt.simulated = true; // distinguish from a normal (nonsimulated) click
  67. e.target.dispatchEvent(evt);
  68. };
  69. /**
  70. * click
  71. *
  72. * Because we've already fired a click event in touchend,
  73. * we need to listed for all native click events here
  74. * and suppress them as necessary.
  75. */
  76. click = function(e) {
  77. /**
  78. * Prevent ghost clicks by only allowing clicks we created
  79. * in the click event we fired (look for e.simulated)
  80. */
  81. var time = Date.now(),
  82. timeDiff = time - lastClick.time,
  83. x = e.clientX,
  84. y = e.clientY,
  85. xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)],
  86. target = closest(e.target, 'A') || e.target, // needed for standalone apps
  87. nodeName = target.nodeName,
  88. isLink = nodeName === 'A',
  89. standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href");
  90. lastClick.time = time;
  91. lastClick.x = x;
  92. lastClick.y = y;
  93. /**
  94. * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire),
  95. * so we have to add more logic to determine the time of the last click. Not perfect...
  96. *
  97. * Older, simpler check: if((!e.simulated) || standAlone)
  98. */
  99. if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) {
  100. e.preventDefault();
  101. e.stopPropagation();
  102. if(!standAlone) return false;
  103. }
  104. /**
  105. * Special logic for standalone web apps
  106. * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window
  107. */
  108. if(standAlone) {
  109. window.location = target.getAttribute("href");
  110. }
  111. /**
  112. * Add an energize-focus class to the targeted link (mimics :focus behavior)
  113. * TODO: test and/or remove? Does this work?
  114. */
  115. if(!target || !target.classList) return;
  116. target.classList.add("energize-focus");
  117. window.setTimeout(function(){
  118. target.classList.remove("energize-focus");
  119. }, 150);
  120. };
  121. /**
  122. * closest
  123. * @param {HTMLElement} node current node to start searching from.
  124. * @param {string} tagName the (uppercase) name of the tag you're looking for.
  125. *
  126. * Find the closest ancestor tag of a given node.
  127. *
  128. * Starts at node and goes up the DOM tree looking for a
  129. * matching nodeName, continuing until hitting document.body
  130. */
  131. closest = function(node, tagName){
  132. var curNode = node;
  133. while(curNode !== document.body) { // go up the dom until we find the tag we're after
  134. if(!curNode || curNode.nodeName === tagName) { return curNode; } // found
  135. curNode = curNode.parentNode; // not found, so keep going up
  136. }
  137. return null; // not found
  138. };
  139. /**
  140. * Add all delegated event listeners
  141. *
  142. * All the events we care about bubble up to document,
  143. * so we can take advantage of event delegation.
  144. *
  145. * Note: no need to wait for DOMContentLoaded here
  146. */
  147. document.addEventListener('touchstart', touchstart, false);
  148. document.addEventListener('touchmove', touchmove, false);
  149. document.addEventListener('touchend', touchend, false);
  150. document.addEventListener('click', click, true); // TODO: why does this use capture?
  151. })();