Update FastClick

pull/168/head
Thibaut 10 years ago
parent 8448461beb
commit 6f260637fa

@ -1,25 +1,26 @@
/**
;(function () {
'use strict';
/**
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
*
* @version 1.0.3
* @codingstandard ftlabs-jsv2
* @copyright The Financial Times Limited [All Rights Reserved]
* @license MIT License (see LICENSE.txt)
*/
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/**
/**
* Instantiate fast-clicking listeners on the specified layer.
*
* @constructor
* @param {Element} layer The layer to listen on
* @param {Object} options The options to override the defaults
* @param {Object} [options={}] The options to override the defaults
*/
function FastClick(layer, options) {
'use strict';
function FastClick(layer, options) {
var oldOnClick;
options = options || {};
@ -94,6 +95,13 @@ function FastClick(layer, options) {
*/
this.tapDelay = options.tapDelay || 200;
/**
* The maximum time for a tap
*
* @type number
*/
this.tapTimeout = options.tapTimeout || 700;
if (FastClick.notNeeded(layer)) {
return;
}
@ -163,55 +171,60 @@ function FastClick(layer, options) {
}, false);
layer.onclick = null;
}
}
}
/**
* Windows Phone 8.1 fakes user agent string to look like Android and iPhone.
*
* @type boolean
*/
var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0;
/**
/**
* Android requires exceptions.
*
* @type boolean
*/
var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone;
/**
/**
* iOS requires exceptions.
*
* @type boolean
*/
var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone;
/**
/**
* iOS 4 requires an exception for select elements.
*
* @type boolean
*/
var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
/**
* iOS 6.0(+?) requires the target element to be manually derived
/**
* iOS 6.0-7.* requires the target element to be manually derived
*
* @type boolean
*/
var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent);
/**
/**
* BlackBerry requires exceptions.
*
* @type boolean
*/
var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
/**
/**
* Determine whether a given element requires a native click.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element needs a native click
*/
FastClick.prototype.needsClick = function(target) {
'use strict';
FastClick.prototype.needsClick = function(target) {
switch (target.nodeName.toLowerCase()) {
// Don't send a synthetic click to disabled inputs (issue #62)
@ -232,22 +245,22 @@ FastClick.prototype.needsClick = function(target) {
break;
case 'label':
case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames
case 'video':
return true;
}
return (/\bneedsclick\b/).test(target.className);
};
};
/**
/**
* Determine whether a given element requires a call to focus to simulate click into element.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
*/
FastClick.prototype.needsFocus = function(target) {
'use strict';
FastClick.prototype.needsFocus = function(target) {
switch (target.nodeName.toLowerCase()) {
case 'textarea':
return true;
@ -269,17 +282,16 @@ FastClick.prototype.needsFocus = function(target) {
default:
return (/\bneedsfocus\b/).test(target.className);
}
};
};
/**
/**
* Send a click event to the specified element.
*
* @param {EventTarget|Element} targetElement
* @param {Event} event
*/
FastClick.prototype.sendClick = function(targetElement, event) {
'use strict';
FastClick.prototype.sendClick = function(targetElement, event) {
var clickEvent, touch;
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
@ -294,10 +306,9 @@ FastClick.prototype.sendClick = function(targetElement, event) {
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};
};
FastClick.prototype.determineEventType = function(targetElement) {
'use strict';
FastClick.prototype.determineEventType = function(targetElement) {
//Issue #159: Android Chrome Select Box does not open with a synthetic click event
if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
@ -305,33 +316,31 @@ FastClick.prototype.determineEventType = function(targetElement) {
}
return 'click';
};
};
/**
/**
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.focus = function(targetElement) {
'use strict';
FastClick.prototype.focus = function(targetElement) {
var length;
// 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.
if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
// Issue #160: on iOS 7, some input elements (e.g. date datetime month) 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.
if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
length = targetElement.value.length;
targetElement.setSelectionRange(length, length);
} else {
targetElement.focus();
}
};
};
/**
/**
* Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
*
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.updateScrollParent = function(targetElement) {
'use strict';
FastClick.prototype.updateScrollParent = function(targetElement) {
var scrollParent, parentElement;
scrollParent = targetElement.fastClickScrollParent;
@ -355,15 +364,14 @@ FastClick.prototype.updateScrollParent = function(targetElement) {
if (scrollParent) {
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
}
};
};
/**
/**
* @param {EventTarget} targetElement
* @returns {Element|EventTarget}
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
'use strict';
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
if (eventTarget.nodeType === Node.TEXT_NODE) {
@ -371,17 +379,16 @@ FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
}
return eventTarget;
};
};
/**
/**
* On touch start, record the position and scroll offset.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchStart = function(event) {
'use strict';
FastClick.prototype.onTouchStart = function(event) {
var targetElement, touch, selection;
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
@ -440,17 +447,16 @@ FastClick.prototype.onTouchStart = function(event) {
}
return true;
};
};
/**
/**
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.touchHasMoved = function(event) {
'use strict';
FastClick.prototype.touchHasMoved = function(event) {
var touch = event.changedTouches[0], boundary = this.touchBoundary;
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
@ -458,17 +464,16 @@ FastClick.prototype.touchHasMoved = function(event) {
}
return false;
};
};
/**
/**
* Update the last position.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchMove = function(event) {
'use strict';
FastClick.prototype.onTouchMove = function(event) {
if (!this.trackingClick) {
return true;
}
@ -480,17 +485,16 @@ FastClick.prototype.onTouchMove = function(event) {
}
return true;
};
};
/**
/**
* Attempt to find the labelled control for the given label element.
*
* @param {EventTarget|HTMLLabelElement} labelElement
* @returns {Element|null}
*/
FastClick.prototype.findControl = function(labelElement) {
'use strict';
FastClick.prototype.findControl = function(labelElement) {
// Fast path for newer browsers supporting the HTML5 control attribute
if (labelElement.control !== undefined) {
@ -505,17 +509,16 @@ FastClick.prototype.findControl = function(labelElement) {
// If no for attribute exists, attempt to retrieve the first labellable descendant element
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};
};
/**
/**
* On touch end, determine whether to send a click event at once.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchEnd = function(event) {
'use strict';
FastClick.prototype.onTouchEnd = function(event) {
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
if (!this.trackingClick) {
@ -528,6 +531,10 @@ FastClick.prototype.onTouchEnd = function(event) {
return true;
}
if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
}
// Reset to prevent wrong click cancel on input (issue #156).
this.cancelNextClick = false;
@ -600,29 +607,27 @@ FastClick.prototype.onTouchEnd = function(event) {
}
return false;
};
};
/**
/**
* On touch cancel, stop tracking the click.
*
* @returns {void}
*/
FastClick.prototype.onTouchCancel = function() {
'use strict';
FastClick.prototype.onTouchCancel = function() {
this.trackingClick = false;
this.targetElement = null;
};
};
/**
/**
* Determine mouse events which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onMouse = function(event) {
'use strict';
FastClick.prototype.onMouse = function(event) {
// If a target element was never set (because a touch event was never fired) allow the event
if (!this.targetElement) {
@ -661,10 +666,10 @@ FastClick.prototype.onMouse = function(event) {
// If the mouse event is permitted, return true for the action to go through.
return true;
};
};
/**
/**
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
* an actual click which should be permitted.
@ -672,8 +677,7 @@ FastClick.prototype.onMouse = function(event) {
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onClick = function(event) {
'use strict';
FastClick.prototype.onClick = function(event) {
var permitted;
// 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.
@ -697,16 +701,15 @@ FastClick.prototype.onClick = function(event) {
// If clicks are permitted, return true for the action to go through.
return permitted;
};
};
/**
/**
* Remove all FastClick's event listeners.
*
* @returns {void}
*/
FastClick.prototype.destroy = function() {
'use strict';
FastClick.prototype.destroy = function() {
var layer = this.layer;
if (deviceIsAndroid) {
@ -720,19 +723,19 @@ FastClick.prototype.destroy = function() {
layer.removeEventListener('touchmove', this.onTouchMove, false);
layer.removeEventListener('touchend', this.onTouchEnd, false);
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
};
};
/**
/**
* Check whether FastClick is needed.
*
* @param {Element} layer The layer to listen on
*/
FastClick.notNeeded = function(layer) {
'use strict';
FastClick.notNeeded = function(layer) {
var metaViewport;
var chromeVersion;
var blackberryVersion;
var firefoxVersion;
// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
@ -785,37 +788,54 @@ FastClick.notNeeded = function(layer) {
}
}
// IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
if (layer.style.msTouchAction === 'none') {
// IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97)
if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
return true;
}
// Firefox version - zero for other browsers
firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
if (firefoxVersion >= 27) {
// Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896
metaViewport = document.querySelector('meta[name=viewport]');
if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {
return true;
}
}
// IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version
// http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx
if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
return true;
}
return false;
};
};
/**
/**
* Factory method for creating a FastClick object
*
* @param {Element} layer The layer to listen on
* @param {Object} options The options to override the defaults
* @param {Object} [options={}] The options to override the defaults
*/
FastClick.attach = function(layer, options) {
'use strict';
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};
};
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
'use strict';
return FastClick;
});
} else if (typeof module !== 'undefined' && module.exports) {
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
} else {
window.FastClick = FastClick;
}
}
}());

Loading…
Cancel
Save