History

Update FastClick

Thibaut committed 12 years ago

Commit 97bfa431 parent 7cc95dd1 file · +531 −547Browse files

Unified · Split

assets/javascripts/vendor/fastclick.js+531−547
@@ -1,7 +1,7 @@
1/**1/**
2 * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.2 * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
3 *3 *
4 * @version 0.6.114 * @version 1.0.0
5 * @codingstandard ftlabs-jsv25 * @codingstandard ftlabs-jsv2
6 * @copyright The Financial Times Limited [All Rights Reserved]6 * @copyright The Financial Times Limited [All Rights Reserved]
7 * @license MIT License (see LICENSE.txt)7 * @license MIT License (see LICENSE.txt)
@@ -18,152 +18,135 @@ · 10 unmodified lines
18 * @param {Element} layer The layer to listen on18 * @param {Element} layer The layer to listen on
19 */19 */
20function FastClick(layer) {20function FastClick(layer) {
21 'use strict';21 'use strict';
22 var oldOnClick, self = this;22 var oldOnClick;
23 23
24 24
25 /**25 /**
26 * Whether a click is currently being tracked.26 * Whether a click is currently being tracked.
27 *27 *
28 * @type boolean28 * @type boolean
29 */29 */
30 this.trackingClick = false;30 this.trackingClick = false;
31 31
32 32
33 /**33 /**
34 * Timestamp for when when click tracking started.34 * Timestamp for when when click tracking started.
35 *35 *
36 * @type number36 * @type number
37 */37 */
38 this.trackingClickStart = 0;38 this.trackingClickStart = 0;
39 39
40 40
41 /**41 /**
42 * The element being tracked for a click.42 * The element being tracked for a click.
43 *43 *
44 * @type EventTarget44 * @type EventTarget
45 */45 */
46 this.targetElement = null;46 this.targetElement = null;
47 47
48 48
49 /**49 /**
50 * X-coordinate of touch start event.50 * X-coordinate of touch start event.
51 *51 *
52 * @type number52 * @type number
53 */53 */
54 this.touchStartX = 0;54 this.touchStartX = 0;
55 55
56 56
57 /**57 /**
58 * Y-coordinate of touch start event.58 * Y-coordinate of touch start event.
59 *59 *
60 * @type number60 * @type number
61 */61 */
62 this.touchStartY = 0;62 this.touchStartY = 0;
63 63
64 64
65 /**65 /**
66 * ID of the last touch, retrieved from Touch.identifier.66 * ID of the last touch, retrieved from Touch.identifier.
67 *67 *
68 * @type number68 * @type number
69 */69 */
70 this.lastTouchIdentifier = 0;70 this.lastTouchIdentifier = 0;
71 71
72 72
73 /**73 /**
74 * Touchmove boundary, beyond which a click will be cancelled.74 * Touchmove boundary, beyond which a click will be cancelled.
75 *75 *
76 * @type number76 * @type number
77 */77 */
78 this.touchBoundary = 10;78 this.touchBoundary = 10;
79 79
80 80
81 /**81 /**
82 * The FastClick layer.82 * The FastClick layer.
83 *83 *
84 * @type Element84 * @type Element
85 */85 */
86 this.layer = layer;86 this.layer = layer;
87 87
88 if (!layer || !layer.nodeType) {88 if (FastClick.notNeeded(layer)) {
89 throw new TypeError('Layer must be a document node');89 return;
90 }90 }
91 91
92 /** @type function() */92 // Some old versions of Android don't have Function.prototype.bind
93 this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };93 function bind(method, context) {
94 94 return function() { return method.apply(context, arguments); };
95 /** @type function() */95 }
96 this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };96
97 97 // Set up event handlers as required
98 /** @type function() */98 if (deviceIsAndroid) {
99 this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };99 layer.addEventListener('mouseover', bind(this.onMouse, this), true);
100 100 layer.addEventListener('mousedown', bind(this.onMouse, this), true);
101 /** @type function() */101 layer.addEventListener('mouseup', bind(this.onMouse, this), true);
102 this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); };102 }
103 103
104 /** @type function() */104 layer.addEventListener('click', bind(this.onClick, this), true);
105 this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };105 layer.addEventListener('touchstart', bind(this.onTouchStart, this), false);
106 106 layer.addEventListener('touchmove', bind(this.onTouchMove, this), false);
107 /** @type function() */107 layer.addEventListener('touchend', bind(this.onTouchEnd, this), false);
108 this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };108 layer.addEventListener('touchcancel', bind(this.onTouchCancel, this), false);
109 109
110 if (FastClick.notNeeded(layer)) {110 // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
111 return;111 // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
112 }112 // layer when they are cancelled.
113 113 if (!Event.prototype.stopImmediatePropagation) {
114 // Set up event handlers as required114 layer.removeEventListener = function(type, callback, capture) {
115 if (this.deviceIsAndroid) {115 var rmv = Node.prototype.removeEventListener;
116 layer.addEventListener('mouseover', this.onMouse, true);116 if (type === 'click') {
117 layer.addEventListener('mousedown', this.onMouse, true);117 rmv.call(layer, type, callback.hijacked || callback, capture);
118 layer.addEventListener('mouseup', this.onMouse, true);118 } else {
119 }119 rmv.call(layer, type, callback, capture);
120 120 }
121 layer.addEventListener('click', this.onClick, true);121 };
122 layer.addEventListener('touchstart', this.onTouchStart, false);122
123 layer.addEventListener('touchmove', this.onTouchMove, false);123 layer.addEventListener = function(type, callback, capture) {
124 layer.addEventListener('touchend', this.onTouchEnd, false);124 var adv = Node.prototype.addEventListener;
125 layer.addEventListener('touchcancel', this.onTouchCancel, false);125 if (type === 'click') {
126 126 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
127 // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)127 if (!event.propagationStopped) {
128 // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick128 callback(event);
129 // layer when they are cancelled.129 }
130 if (!Event.prototype.stopImmediatePropagation) {130 }), capture);
131 layer.removeEventListener = function(type, callback, capture) {131 } else {
132 var rmv = Node.prototype.removeEventListener;132 adv.call(layer, type, callback, capture);
133 if (type === 'click') {133 }
134 rmv.call(layer, type, callback.hijacked || callback, capture);134 };
135 } else {135 }
136 rmv.call(layer, type, callback, capture);136
137 }137 // If a handler is already declared in the element's onclick attribute, it will be fired before
138 };138 // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
139 139 // adding it as listener.
140 layer.addEventListener = function(type, callback, capture) {140 if (typeof layer.onclick === 'function') {
141 var adv = Node.prototype.addEventListener;141
142 if (type === 'click') {142 // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
143 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {143 // - the old one won't work if passed to addEventListener directly.
144 if (!event.propagationStopped) {144 oldOnClick = layer.onclick;
145 callback(event);145 layer.addEventListener('click', function(event) {
146 }146 oldOnClick(event);
147 }), capture);147 }, false);
148 } else {148 layer.onclick = null;
149 adv.call(layer, type, callback, capture);149 }
150 }
151 };
152 }
153
154 // If a handler is already declared in the element's onclick attribute, it will be fired before
155 // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
156 // adding it as listener.
157 if (typeof layer.onclick === 'function') {
158
159 // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
160 // - the old one won't work if passed to addEventListener directly.
161 oldOnClick = layer.onclick;
162 layer.addEventListener('click', function(event) {
163 oldOnClick(event);
164 }, false);
165 layer.onclick = null;
166 }
167}150}
168 151
169 152
@@ -172,7 +155,7 @@ function FastClick(layer) { · 2 unmodified lines
172 *155 *
173 * @type boolean156 * @type boolean
174 */157 */
175FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;158var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
176 159
177 160
178/**161/**
@@ -180,7 +163,7 @@ FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 · 1 unmodified line
180 *163 *
181 * @type boolean164 * @type boolean
182 */165 */
183FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);166var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
184 167
185 168
186/**169/**
@@ -188,7 +171,7 @@ FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent); · 1 unmodified line
188 *171 *
189 * @type boolean172 * @type boolean
190 */173 */
191FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);174var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
192 175
193 176
194/**177/**
@@ -196,7 +179,7 @@ FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d( · 1 unmodified line
196 *179 *
197 * @type boolean180 * @type boolean
198 */181 */
199FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);182var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
200 183
201 184
202/**185/**
@@ -206,32 +189,32 @@ FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS & · 3 unmodified lines
206 * @returns {boolean} Returns true if the element needs a native click189 * @returns {boolean} Returns true if the element needs a native click
207 */190 */
208FastClick.prototype.needsClick = function(target) {191FastClick.prototype.needsClick = function(target) {
209 'use strict';192 'use strict';
210 switch (target.nodeName.toLowerCase()) {193 switch (target.nodeName.toLowerCase()) {
211 194
212 // Don't send a synthetic click to disabled inputs (issue #62)195 // Don't send a synthetic click to disabled inputs (issue #62)
213 case 'button':196 case 'button':
214 case 'select':197 case 'select':
215 case 'textarea':198 case 'textarea':
216 if (target.disabled) {199 if (target.disabled) {
217 return true;200 return true;
218 }201 }
219 202
220 break;203 break;
221 case 'input':204 case 'input':
222 205
223 // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)206 // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
224 if ((this.deviceIsIOS && target.type === 'file') || target.disabled) {207 if ((deviceIsIOS && target.type === 'file') || target.disabled) {
225 return true;208 return true;
226 }209 }
227 210
228 break;211 break;
229 case 'label':212 case 'label':
230 case 'video':213 case 'video':
231 return true;214 return true;
232 }215 }
233 216
234 return (/\bneedsclick\b/).test(target.className);217 return (/\bneedsclick\b/).test(target.className);
235};218};
236 219
237 220
@@ -242,28 +225,28 @@ FastClick.prototype.needsClick = function(target) { · 4 unmodified lines
242 * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.225 * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
243 */226 */
244FastClick.prototype.needsFocus = function(target) {227FastClick.prototype.needsFocus = function(target) {
245 'use strict';228 'use strict';
246 switch (target.nodeName.toLowerCase()) {229 switch (target.nodeName.toLowerCase()) {
247 case 'textarea':230 case 'textarea':
248 return true;231 return true;
249 case 'select':232 case 'select':
250 return !this.deviceIsAndroid;233 return !deviceIsAndroid;
251 case 'input':234 case 'input':
252 switch (target.type) {235 switch (target.type) {
253 case 'button':236 case 'button':
254 case 'checkbox':237 case 'checkbox':
255 case 'file':238 case 'file':
256 case 'image':239 case 'image':
257 case 'radio':240 case 'radio':
258 case 'submit':241 case 'submit':
259 return false;242 return false;
260 }243 }
261 244
262 // No point in attempting to focus disabled inputs245 // No point in attempting to focus disabled inputs
263 return !target.disabled && !target.readOnly;246 return !target.disabled && !target.readOnly;
264 default:247 default:
265 return (/\bneedsfocus\b/).test(target.className);248 return (/\bneedsfocus\b/).test(target.className);
266 }249 }
267};250};
268 251
269 252
@@ -274,32 +257,32 @@ FastClick.prototype.needsFocus = function(target) { · 4 unmodified lines
274 * @param {Event} event257 * @param {Event} event
275 */258 */
276FastClick.prototype.sendClick = function(targetElement, event) {259FastClick.prototype.sendClick = function(targetElement, event) {
277 'use strict';260 'use strict';
278 var clickEvent, touch;261 var clickEvent, touch;
279 262
280 // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)263 // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
281 if (document.activeElement && document.activeElement !== targetElement) {264 if (document.activeElement && document.activeElement !== targetElement) {
282 document.activeElement.blur();265 document.activeElement.blur();
283 }266 }
284 267
285 touch = event.changedTouches[0];268 touch = event.changedTouches[0];
286 269
287 // Synthesise a click event, with an extra attribute so it can be tracked270 // Synthesise a click event, with an extra attribute so it can be tracked
288 clickEvent = document.createEvent('MouseEvents');271 clickEvent = document.createEvent('MouseEvents');
289 clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);272 clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
290 clickEvent.forwardedTouchEvent = true;273 clickEvent.forwardedTouchEvent = true;
291 targetElement.dispatchEvent(clickEvent);274 targetElement.dispatchEvent(clickEvent);
292};275};
293 276
294FastClick.prototype.determineEventType = function(targetElement) {277FastClick.prototype.determineEventType = function(targetElement) {
295 'use strict';278 'use strict';
296 279
297 //Issue #159: Android Chrome Select Box does not open with a synthetic click event280 //Issue #159: Android Chrome Select Box does not open with a synthetic click event
298 if (this.deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {281 if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
299 return 'mousedown';282 return 'mousedown';
300 }283 }
301 284
302 return 'click';285 return 'click';
303};286};
304 287
305 288
@@ -307,16 +290,16 @@ FastClick.prototype.determineEventType = function(targetElement) { · 1 unmodified line
307 * @param {EventTarget|Element} targetElement290 * @param {EventTarget|Element} targetElement
308 */291 */
309FastClick.prototype.focus = function(targetElement) {292FastClick.prototype.focus = function(targetElement) {
310 'use strict';293 'use strict';
311 var length;294 var length;
312 295
313 // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.296 // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
314 if (this.deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {297 if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
315 length = targetElement.value.length;298 length = targetElement.value.length;
316 targetElement.setSelectionRange(length, length);299 targetElement.setSelectionRange(length, length);
317 } else {300 } else {
318 targetElement.focus();301 targetElement.focus();
319 }302 }
320};303};
321 304
322 305
@@ -326,30 +309,30 @@ FastClick.prototype.focus = function(targetElement) { · 3 unmodified lines
326 * @param {EventTarget|Element} targetElement309 * @param {EventTarget|Element} targetElement
327 */310 */
328FastClick.prototype.updateScrollParent = function(targetElement) {311FastClick.prototype.updateScrollParent = function(targetElement) {
329 'use strict';312 'use strict';
330 var scrollParent, parentElement;313 var scrollParent, parentElement;
331 314
332 scrollParent = targetElement.fastClickScrollParent;315 scrollParent = targetElement.fastClickScrollParent;
333 316
334 // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the317 // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
335 // target element was moved to another parent.318 // target element was moved to another parent.
336 if (!scrollParent || !scrollParent.contains(targetElement)) {319 if (!scrollParent || !scrollParent.contains(targetElement)) {
337 parentElement = targetElement;320 parentElement = targetElement;
338 do {321 do {
339 if (parentElement.scrollHeight > parentElement.offsetHeight) {322 if (parentElement.scrollHeight > parentElement.offsetHeight) {
340 scrollParent = parentElement;323 scrollParent = parentElement;
341 targetElement.fastClickScrollParent = parentElement;324 targetElement.fastClickScrollParent = parentElement;
342 break;325 break;
343 }326 }
344 327
345 parentElement = parentElement.parentElement;328 parentElement = parentElement.parentElement;
346 } while (parentElement);329 } while (parentElement);
347 }330 }
348 331
349 // Always update the scroll top tracker if possible.332 // Always update the scroll top tracker if possible.
350 if (scrollParent) {333 if (scrollParent) {
351 scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;334 scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
352 }335 }
353};336};
354 337
355 338
@@ -358,14 +341,14 @@ FastClick.prototype.updateScrollParent = function(targetElement) { · 2 unmodified lines
358 * @returns {Element|EventTarget}341 * @returns {Element|EventTarget}
359 */342 */
360FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {343FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
361 'use strict';344 'use strict';
362 345
363 // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.346 // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
364 if (eventTarget.nodeType === Node.TEXT_NODE) {347 if (eventTarget.nodeType === Node.TEXT_NODE) {
365 return eventTarget.parentNode;348 return eventTarget.parentNode;
366 }349 }
367 350
368 return eventTarget;351 return eventTarget;
369};352};
370 353
371 354
@@ -376,62 +359,62 @@ FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { · 4 unmodified lines
376 * @returns {boolean}359 * @returns {boolean}
377 */360 */
378FastClick.prototype.onTouchStart = function(event) {361FastClick.prototype.onTouchStart = function(event) {
379 'use strict';362 'use strict';
380 var targetElement, touch, selection;363 var targetElement, touch, selection;
381 364
382 // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).365 // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
383 if (event.targetTouches.length > 1) {366 if (event.targetTouches.length > 1) {
384 return true;367 return true;
385 }368 }
386 369
387 targetElement = this.getTargetElementFromEventTarget(event.target);370 targetElement = this.getTargetElementFromEventTarget(event.target);
388 touch = event.targetTouches[0];371 touch = event.targetTouches[0];
389 372
390 if (this.deviceIsIOS) {373 if (deviceIsIOS) {
391 374
392 // Only trusted events will deselect text on iOS (issue #49)375 // Only trusted events will deselect text on iOS (issue #49)
393 selection = window.getSelection();376 selection = window.getSelection();
394 if (selection.rangeCount && !selection.isCollapsed) {377 if (selection.rangeCount && !selection.isCollapsed) {
395 return true;378 return true;
396 }379 }
397 380
398 if (!this.deviceIsIOS4) {381 if (!deviceIsIOS4) {
399 382
400 // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):383 // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
401 // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched384 // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
402 // with the same identifier as the touch event that previously triggered the click that triggered the alert.385 // with the same identifier as the touch event that previously triggered the click that triggered the alert.
403 // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an386 // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
404 // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.387 // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
405 if (touch.identifier === this.lastTouchIdentifier) {388 if (touch.identifier === this.lastTouchIdentifier) {
406 event.preventDefault();389 event.preventDefault();
407 return false;390 return false;
408 }391 }
409 392
410 this.lastTouchIdentifier = touch.identifier;393 this.lastTouchIdentifier = touch.identifier;
411 394
412 // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:395 // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
413 // 1) the user does a fling scroll on the scrollable layer396 // 1) the user does a fling scroll on the scrollable layer
414 // 2) the user stops the fling scroll with another tap397 // 2) the user stops the fling scroll with another tap
415 // then the event.target of the last 'touchend' event will be the element that was under the user's finger398 // then the event.target of the last 'touchend' event will be the element that was under the user's finger
416 // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check399 // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
417 // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).400 // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
418 this.updateScrollParent(targetElement);401 this.updateScrollParent(targetElement);
419 }402 }
420 }403 }
421 404
422 this.trackingClick = true;405 this.trackingClick = true;
423 this.trackingClickStart = event.timeStamp;406 this.trackingClickStart = event.timeStamp;
424 this.targetElement = targetElement;407 this.targetElement = targetElement;
425 408
426 this.touchStartX = touch.pageX;409 this.touchStartX = touch.pageX;
427 this.touchStartY = touch.pageY;410 this.touchStartY = touch.pageY;
428 411
429 // Prevent phantom clicks on fast double-tap (issue #36)412 // Prevent phantom clicks on fast double-tap (issue #36)
430 if ((event.timeStamp - this.lastClickTime) < 200) {413 if ((event.timeStamp - this.lastClickTime) < 200) {
431 event.preventDefault();414 event.preventDefault();
432 }415 }
433 416
434 return true;417 return true;
435};418};
436 419
437 420
@@ -442,14 +425,14 @@ FastClick.prototype.onTouchStart = function(event) { · 4 unmodified lines
442 * @returns {boolean}425 * @returns {boolean}
443 */426 */
444FastClick.prototype.touchHasMoved = function(event) {427FastClick.prototype.touchHasMoved = function(event) {
445 'use strict';428 'use strict';
446 var touch = event.changedTouches[0], boundary = this.touchBoundary;429 var touch = event.changedTouches[0], boundary = this.touchBoundary;
447 430
448 if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {431 if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
449 return true;432 return true;
450 }433 }
451 434
452 return false;435 return false;
453};436};
454 437
455 438
@@ -460,18 +443,18 @@ FastClick.prototype.touchHasMoved = function(event) { · 4 unmodified lines
460 * @returns {boolean}443 * @returns {boolean}
461 */444 */
462FastClick.prototype.onTouchMove = function(event) {445FastClick.prototype.onTouchMove = function(event) {
463 'use strict';446 'use strict';
464 if (!this.trackingClick) {447 if (!this.trackingClick) {
465 return true;448 return true;
466 }449 }
467 450
468 // If the touch has moved, cancel the click tracking451 // If the touch has moved, cancel the click tracking
469 if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {452 if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
470 this.trackingClick = false;453 this.trackingClick = false;
471 this.targetElement = null;454 this.targetElement = null;
472 }455 }
473 456
474 return true;457 return true;
475};458};
476 459
477 460
@@ -482,21 +465,21 @@ FastClick.prototype.onTouchMove = function(event) { · 4 unmodified lines
482 * @returns {Element|null}465 * @returns {Element|null}
483 */466 */
484FastClick.prototype.findControl = function(labelElement) {467FastClick.prototype.findControl = function(labelElement) {
485 'use strict';468 'use strict';
486 469
487 // Fast path for newer browsers supporting the HTML5 control attribute470 // Fast path for newer browsers supporting the HTML5 control attribute
488 if (labelElement.control !== undefined) {471 if (labelElement.control !== undefined) {
489 return labelElement.control;472 return labelElement.control;
490 }473 }
491 474
492 // All browsers under test that support touch events also support the HTML5 htmlFor attribute475 // All browsers under test that support touch events also support the HTML5 htmlFor attribute
493 if (labelElement.htmlFor) {476 if (labelElement.htmlFor) {
494 return document.getElementById(labelElement.htmlFor);477 return document.getElementById(labelElement.htmlFor);
495 }478 }
496 479
497 // If no for attribute exists, attempt to retrieve the first labellable descendant element480 // If no for attribute exists, attempt to retrieve the first labellable descendant element
498 // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label481 // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
499 return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');482 return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
500};483};
501 484
502 485
@@ -507,89 +490,90 @@ FastClick.prototype.findControl = function(labelElement) { · 4 unmodified lines
507 * @returns {boolean}490 * @returns {boolean}
508 */491 */
509FastClick.prototype.onTouchEnd = function(event) {492FastClick.prototype.onTouchEnd = function(event) {
510 'use strict';493 'use strict';
511 var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;494 var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
512 495
513 if (!this.trackingClick) {496 if (!this.trackingClick) {
514 return true;497 return true;
515 }498 }
516 499
517 // Prevent phantom clicks on fast double-tap (issue #36)500 // Prevent phantom clicks on fast double-tap (issue #36)
518 if ((event.timeStamp - this.lastClickTime) < 200) {501 if ((event.timeStamp - this.lastClickTime) < 200) {
519 this.cancelNextClick = true;502 this.cancelNextClick = true;
520 return true;503 return true;
521 }504 }
522 505
523 // Reset to prevent wrong click cancel on input (issue #156).506 // Reset to prevent wrong click cancel on input (issue #156).
524 this.cancelNextClick = false;507 this.cancelNextClick = false;
525 508
526 this.lastClickTime = event.timeStamp;509 this.lastClickTime = event.timeStamp;
527 510
528 trackingClickStart = this.trackingClickStart;511 trackingClickStart = this.trackingClickStart;
529 this.trackingClick = false;512 this.trackingClick = false;
530 this.trackingClickStart = 0;513 this.trackingClickStart = 0;
531 514
532 // On some iOS devices, the targetElement supplied with the event is invalid if the layer515 // On some iOS devices, the targetElement supplied with the event is invalid if the layer
533 // is performing a transition or scroll, and has to be re-detected manually. Note that516 // is performing a transition or scroll, and has to be re-detected manually. Note that
534 // for this to function correctly, it must be called *after* the event target is checked!517 // for this to function correctly, it must be called *after* the event target is checked!
535 // See issue #57; also filed as rdar://13048589 .518 // See issue #57; also filed as rdar://13048589 .
536 if (this.deviceIsIOSWithBadTarget) {519 if (deviceIsIOSWithBadTarget) {
537 touch = event.changedTouches[0];520 touch = event.changedTouches[0];
538 521
539 // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null522 // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
540 targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;523 targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
541 targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;524 targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
542 }525 }
543 526
544 targetTagName = targetElement.tagName.toLowerCase();527 targetTagName = targetElement.tagName.toLowerCase();
545 if (targetTagName === 'label') {528 if (targetTagName === 'label') {
546 forElement = this.findControl(targetElement);529 forElement = this.findControl(targetElement);
547 if (forElement) {530 if (forElement) {
548 this.focus(targetElement);531 this.focus(targetElement);
549 if (this.deviceIsAndroid) {532 if (deviceIsAndroid) {
550 return false;533 return false;
551 }534 }
552 535
553 targetElement = forElement;536 targetElement = forElement;
554 }537 }
555 } else if (this.needsFocus(targetElement)) {538 } else if (this.needsFocus(targetElement)) {
556 539
557 // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.540 // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
558 // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).541 // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
559 if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {542 if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
560 this.targetElement = null;543 this.targetElement = null;
561 return false;544 return false;
562 }545 }
563 546
564 this.focus(targetElement);547 this.focus(targetElement);
565 548 this.sendClick(targetElement, event);
566 // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.549
567 if (!this.deviceIsIOS4 || targetTagName !== 'select') {550 // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
568 this.targetElement = null;551 if (!deviceIsIOS4 || targetTagName !== 'select') {
569 event.preventDefault();552 this.targetElement = null;
570 }553 event.preventDefault();
571 554 }
572 return false;555
573 }556 return false;
574 557 }
575 if (this.deviceIsIOS && !this.deviceIsIOS4) {558
576 559 if (deviceIsIOS && !deviceIsIOS4) {
577 // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled560
578 // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).561 // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
579 scrollParent = targetElement.fastClickScrollParent;562 // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
580 if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {563 scrollParent = targetElement.fastClickScrollParent;
581 return true;564 if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
582 }565 return true;
583 }566 }
584 567 }
585 // Prevent the actual click from going though - unless the target node is marked as requiring568
586 // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.569 // Prevent the actual click from going though - unless the target node is marked as requiring
587 if (!this.needsClick(targetElement)) {570 // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
588 event.preventDefault();571 if (!this.needsClick(targetElement)) {
589 this.sendClick(targetElement, event);572 event.preventDefault();
590 }573 this.sendClick(targetElement, event);
591 574 }
592 return false;575
576 return false;
593};577};
594 578
595 579
@@ -599,9 +583,9 @@ FastClick.prototype.onTouchEnd = function(event) { · 3 unmodified lines
599 * @returns {void}583 * @returns {void}
600 */584 */
601FastClick.prototype.onTouchCancel = function() {585FastClick.prototype.onTouchCancel = function() {
602 'use strict';586 'use strict';
603 this.trackingClick = false;587 this.trackingClick = false;
604 this.targetElement = null;588 this.targetElement = null;
605};589};
606 590
607 591
@@ -612,45 +596,45 @@ FastClick.prototype.onTouchCancel = function() { · 4 unmodified lines
612 * @returns {boolean}596 * @returns {boolean}
613 */597 */
614FastClick.prototype.onMouse = function(event) {598FastClick.prototype.onMouse = function(event) {
615 'use strict';599 'use strict';
616 600
617 // If a target element was never set (because a touch event was never fired) allow the event601 // If a target element was never set (because a touch event was never fired) allow the event
618 if (!this.targetElement) {602 if (!this.targetElement) {
619 return true;603 return true;
620 }604 }
621 605
622 if (event.forwardedTouchEvent) {606 if (event.forwardedTouchEvent) {
623 return true;607 return true;
624 }608 }
625 609
626 // Programmatically generated events targeting a specific element should be permitted610 // Programmatically generated events targeting a specific element should be permitted
627 if (!event.cancelable) {611 if (!event.cancelable) {
628 return true;612 return true;
629 }613 }
630 614
631 // Derive and check the target element to see whether the mouse event needs to be permitted;615 // Derive and check the target element to see whether the mouse event needs to be permitted;
632 // unless explicitly enabled, prevent non-touch click events from triggering actions,616 // unless explicitly enabled, prevent non-touch click events from triggering actions,
633 // to prevent ghost/doubleclicks.617 // to prevent ghost/doubleclicks.
634 if (!this.needsClick(this.targetElement) || this.cancelNextClick) {618 if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
635 619
636 // Prevent any user-added listeners declared on FastClick element from being fired.620 // Prevent any user-added listeners declared on FastClick element from being fired.
637 if (event.stopImmediatePropagation) {621 if (event.stopImmediatePropagation) {
638 event.stopImmediatePropagation();622 event.stopImmediatePropagation();
639 } else {623 } else {
640 624
641 // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)625 // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
642 event.propagationStopped = true;626 event.propagationStopped = true;
643 }627 }
644 628
645 // Cancel the event629 // Cancel the event
646 event.stopPropagation();630 event.stopPropagation();
647 event.preventDefault();631 event.preventDefault();
648 632
649 return false;633 return false;
650 }634 }
651 635
652 // If the mouse event is permitted, return true for the action to go through.636 // If the mouse event is permitted, return true for the action to go through.
653 return true;637 return true;
654};638};
655 639
656 640
@@ -663,30 +647,30 @@ FastClick.prototype.onMouse = function(event) { · 6 unmodified lines
663 * @returns {boolean}647 * @returns {boolean}
664 */648 */
665FastClick.prototype.onClick = function(event) {649FastClick.prototype.onClick = function(event) {
666 'use strict';650 'use strict';
667 var permitted;651 var permitted;
668 652
669 // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.653 // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
670 if (this.trackingClick) {654 if (this.trackingClick) {
671 this.targetElement = null;655 this.targetElement = null;
672 this.trackingClick = false;656 this.trackingClick = false;
673 return true;657 return true;
674 }658 }
675 659
676 // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.660 // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
677 if (event.target.type === 'submit' && event.detail === 0) {661 if (event.target.type === 'submit' && event.detail === 0) {
678 return true;662 return true;
679 }663 }
680 664
681 permitted = this.onMouse(event);665 permitted = this.onMouse(event);
682 666
683 // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.667 // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
684 if (!permitted) {668 if (!permitted) {
685 this.targetElement = null;669 this.targetElement = null;
686 }670 }
687 671
688 // If clicks are permitted, return true for the action to go through.672 // If clicks are permitted, return true for the action to go through.
689 return permitted;673 return permitted;
690};674};
691 675
692 676
@@ -696,20 +680,20 @@ FastClick.prototype.onClick = function(event) { · 3 unmodified lines
696 * @returns {void}680 * @returns {void}
697 */681 */
698FastClick.prototype.destroy = function() {682FastClick.prototype.destroy = function() {
699 'use strict';683 'use strict';
700 var layer = this.layer;684 var layer = this.layer;
701 685
702 if (this.deviceIsAndroid) {686 if (deviceIsAndroid) {
703 layer.removeEventListener('mouseover', this.onMouse, true);687 layer.removeEventListener('mouseover', this.onMouse, true);
704 layer.removeEventListener('mousedown', this.onMouse, true);688 layer.removeEventListener('mousedown', this.onMouse, true);
705 layer.removeEventListener('mouseup', this.onMouse, true);689 layer.removeEventListener('mouseup', this.onMouse, true);
706 }690 }
707 691
708 layer.removeEventListener('click', this.onClick, true);692 layer.removeEventListener('click', this.onClick, true);
709 layer.removeEventListener('touchstart', this.onTouchStart, false);693 layer.removeEventListener('touchstart', this.onTouchStart, false);
710 layer.removeEventListener('touchmove', this.onTouchMove, false);694 layer.removeEventListener('touchmove', this.onTouchMove, false);
711 layer.removeEventListener('touchend', this.onTouchEnd, false);695 layer.removeEventListener('touchend', this.onTouchEnd, false);
712 layer.removeEventListener('touchcancel', this.onTouchCancel, false);696 layer.removeEventListener('touchcancel', this.onTouchCancel, false);
713};697};
714 698
715 699
@@ -719,46 +703,46 @@ FastClick.prototype.destroy = function() { · 3 unmodified lines
719 * @param {Element} layer The layer to listen on703 * @param {Element} layer The layer to listen on
720 */704 */
721FastClick.notNeeded = function(layer) {705FastClick.notNeeded = function(layer) {
722 'use strict';706 'use strict';
723 var metaViewport;707 var metaViewport;
724 var chromeVersion;708 var chromeVersion;
725 709
726 // Devices that don't support touch don't need FastClick710 // Devices that don't support touch don't need FastClick
727 if (typeof window.ontouchstart === 'undefined') {711 if (typeof window.ontouchstart === 'undefined') {
728 return true;712 return true;
729 }713 }
730 714
731 // Chrome version - zero for other browsers715 // Chrome version - zero for other browsers
732 chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];716 chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
733 717
734 if (chromeVersion) {718 if (chromeVersion) {
735 719
736 if (FastClick.prototype.deviceIsAndroid) {720 if (deviceIsAndroid) {
737 metaViewport = document.querySelector('meta[name=viewport]');721 metaViewport = document.querySelector('meta[name=viewport]');
738 722
739 if (metaViewport) {723 if (metaViewport) {
740 // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)724 // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
741 if (metaViewport.content.indexOf('user-scalable=no') !== -1) {725 if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
742 return true;726 return true;
743 }727 }
744 // Chrome 32 and above with width=device-width or less don't need FastClick728 // Chrome 32 and above with width=device-width or less don't need FastClick
745 if (chromeVersion > 31 && window.innerWidth <= window.screen.width) {729 if (chromeVersion > 31 && window.innerWidth <= window.screen.width) {
746 return true;730 return true;
747 }731 }
748 }732 }
749 733
750 // Chrome desktop doesn't need FastClick (issue #15)734 // Chrome desktop doesn't need FastClick (issue #15)
751 } else {735 } else {
752 return true;736 return true;
753 }737 }
754 }738 }
755 739
756 // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)740 // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
757 if (layer.style.msTouchAction === 'none') {741 if (layer.style.msTouchAction === 'none') {
758 return true;742 return true;
759 }743 }
760 744
761 return false;745 return false;
762};746};
763 747
764 748
@@ -768,21 +752,21 @@ FastClick.notNeeded = function(layer) { · 3 unmodified lines
768 * @param {Element} layer The layer to listen on752 * @param {Element} layer The layer to listen on
769 */753 */
770FastClick.attach = function(layer) {754FastClick.attach = function(layer) {
771 'use strict';755 'use strict';
772 return new FastClick(layer);756 return new FastClick(layer);
773};757};
774 758
775 759
776if (typeof define !== 'undefined' && define.amd) {760if (typeof define !== 'undefined' && define.amd) {
777 761
778 // AMD. Register as an anonymous module.762 // AMD. Register as an anonymous module.
779 define(function() {763 define(function() {
780 'use strict';764 'use strict';
781 return FastClick;765 return FastClick;
782 });766 });
783} else if (typeof module !== 'undefined' && module.exports) {767} else if (typeof module !== 'undefined' && module.exports) {
784 module.exports = FastClick.attach;768 module.exports = FastClick.attach;
785 module.exports.FastClick = FastClick;769 module.exports.FastClick = FastClick;
786} else {770} else {
787 window.FastClick = FastClick;771 window.FastClick = FastClick;
788}772}