fastclick.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /**
  2. * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
  3. *
  4. * @copyright The Financial Times Limited [All Rights Reserved]
  5. * @license MIT License (see LICENCE.txt)
  6. * @codingstandard ftlabs-jslint
  7. */
  8. /*jslint browser:true*/
  9. /*global Node, define*/
  10. (function() {
  11. 'use strict';
  12. var
  13. /**
  14. * Android requires an exception for labels.
  15. *
  16. * @type boolean
  17. */
  18. android = navigator.userAgent.indexOf('Android') > 0,
  19. /**
  20. * Earlier versions of Chrome for Android don't report themselves as "Chrome" but "CrMo" - check for both.
  21. *
  22. * @type boolean
  23. */
  24. chromeAndroid = android && (/Chrome|CrMo/).test(navigator.userAgent),
  25. /**
  26. * Playbook requires a greater scroll boundary.
  27. *
  28. * @type number
  29. */
  30. scrollBoundary = (android || (navigator.userAgent.indexOf('PlayBook') === -1)) ? 5 : 20;
  31. /**
  32. * Determine whether a given element requires a native click.
  33. *
  34. * @param {Element} target DOM element
  35. * @returns {boolean} Returns true if the element needs a native click
  36. */
  37. function needsClick(target) {
  38. switch (target.nodeName.toLowerCase()) {
  39. case 'label':
  40. case 'video':
  41. return true;
  42. default:
  43. return (/\bneedsclick\b/).test(target.className);
  44. }
  45. }
  46. /**
  47. * Determine whether a given element requires a call to focus to simulate click into element.
  48. *
  49. * @param {Element} target target DOM element.
  50. * @return {boolean} Returns true if the element requires a call to focus to simulate native click.
  51. */
  52. function needsFocus(target) {
  53. switch(target.nodeName.toLowerCase()) {
  54. case 'textarea':
  55. case 'select':
  56. return true;
  57. case 'input':
  58. switch (target.type) {
  59. case 'button':
  60. case 'checkbox':
  61. case 'file':
  62. case 'image':
  63. case 'radio':
  64. case 'submit':
  65. return false;
  66. default:
  67. return true;
  68. }
  69. break;
  70. default:
  71. return (/\bneedsfocus\b/).test(target.className);
  72. }
  73. }
  74. /**
  75. * Retrieve an element based on coordinates within the window.
  76. *
  77. * @param {number} x
  78. * @param {number} y
  79. * @return {Element}
  80. */
  81. function eleAtWindowPosition(x, y) {
  82. // On Chrome for Android, amend coordinates by the device pixel ratio.
  83. if (chromeAndroid && window.devicePixelRatio) {
  84. x *= window.devicePixelRatio;
  85. y *= window.devicePixelRatio;
  86. }
  87. return document.elementFromPoint(x, y);
  88. }
  89. /**
  90. * Instantiate fast-clicking listeners on the specificed layer.
  91. *
  92. * @constructor
  93. * @param {Element} layer The layer to listen on
  94. */
  95. function FastClick(layer) {
  96. var
  97. /**
  98. * @type Function
  99. */
  100. oldOnClick,
  101. /**
  102. * The position and page scroll amount when click had started to be tracked.
  103. *
  104. * @type Object
  105. */
  106. clickStart = { x: 0, y: 0, scrollX: 0, scrollY: 0 },
  107. /**
  108. * Whether a click is currently being tracked.
  109. *
  110. * @type boolean
  111. */
  112. trackingClick = false,
  113. /**
  114. * Maximum distance (37 pixels) to the power of two.
  115. *
  116. * @type number
  117. */
  118. bound = Math.pow(37, 2),
  119. /**
  120. * On touch start, record the position and scroll offset.
  121. *
  122. * @param {Event} event
  123. * @returns {boolean}
  124. */
  125. onTouchStart = function(event) {
  126. trackingClick = true;
  127. clickStart.x = event.targetTouches[0].pageX;
  128. clickStart.y = event.targetTouches[0].pageY;
  129. if (clickStart.x === event.targetTouches[0].clientX) {
  130. clickStart.x += window.pageXOffset;
  131. }
  132. if (clickStart.y === event.targetTouches[0].clientY) {
  133. clickStart.y += window.pageYOffset;
  134. }
  135. clickStart.scrollX = window.pageXOffset;
  136. clickStart.scrollY = window.pageYOffset;
  137. return true;
  138. },
  139. /**
  140. * Update the last position.
  141. *
  142. * @param {Event} event
  143. * @returns {boolean}
  144. */
  145. onTouchMove = function(event) {
  146. if (!trackingClick) {
  147. return true;
  148. }
  149. // Detect whether a click has left the bounds of would be defined as a click, defined as a circle of radius sqrt(bound) around the start point.
  150. if ((Math.pow(event.targetTouches[0].pageX - clickStart.x, 2) + Math.pow(event.targetTouches[0].pageY - clickStart.y, 2)) > bound) {
  151. trackingClick = false;
  152. }
  153. // If the touch has moved, cancel the click tracking
  154. if (Math.abs(window.pageXOffset - clickStart.scrollX) > scrollBoundary || Math.abs(window.pageYOffset - clickStart.scrollY) > scrollBoundary) {
  155. trackingClick = false;
  156. }
  157. return true;
  158. },
  159. /**
  160. * On touch end, determine whether to send a click event at once.
  161. *
  162. * @param {Event} event
  163. * @returns {boolean}
  164. */
  165. onTouchEnd = function(event) {
  166. var targetElement, forElement, targetCoordinates, clickEvent;
  167. if (!trackingClick) {
  168. return true;
  169. }
  170. trackingClick = false;
  171. // Set up the coordinates to match
  172. targetCoordinates = {
  173. x: clickStart.x - clickStart.scrollX,
  174. y: clickStart.y - clickStart.scrollY
  175. };
  176. // Derive the element to click as a result of the touch.
  177. targetElement = eleAtWindowPosition(targetCoordinates.x, targetCoordinates.y);
  178. // If we're not clicking anything exit early
  179. if (!targetElement) {
  180. return false;
  181. }
  182. // If the targetted node is a text node, target the parent instead
  183. if (targetElement.nodeType === Node.TEXT_NODE) {
  184. targetElement = targetElement.parentElement;
  185. }
  186. if (targetElement.nodeName.toLowerCase() === 'label' && targetElement.htmlFor) {
  187. forElement = document.getElementById(targetElement.htmlFor);
  188. if (forElement) {
  189. targetElement.focus();
  190. if (android) {
  191. return false;
  192. }
  193. targetElement = forElement;
  194. }
  195. } else if (needsFocus(targetElement)) {
  196. targetElement.focus();
  197. return false;
  198. }
  199. // Prevent the actual click from going though - unless the target node is marked as requiring
  200. // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted
  201. // to open the options list and so the original event is required.
  202. if (needsClick(targetElement)) {
  203. return false;
  204. }
  205. // Synthesise a click event, with an extra attribute so it can be tracked
  206. clickEvent = document.createEvent('MouseEvents');
  207. clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, targetCoordinates.x, targetCoordinates.y, false, false, false, false, 0, null);
  208. clickEvent.forwardedTouchEvent = true;
  209. targetElement.dispatchEvent(clickEvent);
  210. event.preventDefault();
  211. return false;
  212. },
  213. /**
  214. * On touch cancel, stop tracking the click.
  215. */
  216. onTouchCancel = function() {
  217. trackingClick = false;
  218. },
  219. /**
  220. * On actual clicks, determine whether this is a touch-generated click, a click action occurring
  221. * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
  222. * an actual click which should be permitted.
  223. *
  224. * @param {Event} event
  225. * @returns {boolean}
  226. */
  227. onClick = function(event) {
  228. var targetElement;
  229. if (event.forwardedTouchEvent) {
  230. return true;
  231. }
  232. // Programmatically generated events targeting a specific element should be permitted
  233. if (!event.cancelable) {
  234. return true;
  235. }
  236. targetElement = eleAtWindowPosition(clickStart.x - clickStart.scrollX, clickStart.y - clickStart.scrollY);
  237. // Derive and check the target element to see whether the click needs to be permitted;
  238. // unless explicitly enabled, prevent non-touch click events from triggering actions,
  239. // to prevent ghost/doubleclicks.
  240. if (!targetElement || !needsClick(targetElement)) {
  241. // Prevent any user-added listeners declared on FastClick element from being fired.
  242. if (event.stopImmediatePropagation) {
  243. event.stopImmediatePropagation();
  244. }
  245. // Cancel the event
  246. event.stopPropagation();
  247. event.preventDefault();
  248. return false;
  249. }
  250. // If clicks are permitted, return true for the action to go through.
  251. return true;
  252. };
  253. if (!layer || !layer.nodeType) {
  254. throw new TypeError('Layer must be a document node');
  255. }
  256. // Devices that don't support touch don't need FastClick
  257. if (typeof window.ontouchstart === 'undefined') {
  258. return;
  259. }
  260. // Set up event handlers as required
  261. layer.addEventListener('click', onClick, true);
  262. layer.addEventListener('touchstart', onTouchStart, true);
  263. layer.addEventListener('touchmove', onTouchMove, true);
  264. layer.addEventListener('touchend', onTouchEnd, true);
  265. layer.addEventListener('touchcancel', onTouchCancel, true);
  266. // If a handler is already declared in the element's onclick attribute, it will be fired before
  267. // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
  268. // adding it as listener.
  269. if (typeof layer.onclick === 'function') {
  270. // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
  271. // - the old one won't work if passed to addEventListener directly.
  272. oldOnClick = layer.onclick;
  273. layer.addEventListener('click', function(event) {
  274. oldOnClick(event);
  275. }, false);
  276. layer.onclick = null;
  277. }
  278. }
  279. if (typeof define === 'function' && define.amd) {
  280. // AMD. Register as an anonymous module.
  281. define(function() {
  282. return FastClick;
  283. });
  284. } else {
  285. // Browser global
  286. window.FastClick = FastClick;
  287. }
  288. }());