;(function () {
'use strict';
/**
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
*
* @codingstandard ftlabs-jsv2
* @copyright The Financial Times Limited [All Rights Reserved]
* @license MIT License (see LICENSE.txt)
*/
/**
* 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
*/
function FastClick(layer, options) {
var oldOnClick;
options = options || {};
/**
* Whether a click is currently being tracked.
*
* @type boolean
*/
this.trackingClick =
false;
/**
* Timestamp for when click tracking started.
*
* @type number
*/
this.trackingClickStart =
0;
/**
* The element being tracked for a click.
*
* @type EventTarget
*/
this.targetElement =
null;
/**
* X-coordinate of touch start event.
*
* @type number
*/
this.touchStartX =
0;
/**
* Y-coordinate of touch start event.
*
* @type number
*/
this.touchStartY =
0;
/**
* ID of the last touch, retrieved from Touch.identifier.
*
* @type number
*/
this.lastTouchIdentifier =
0;
/**
* Touchmove boundary, beyond which a click will be cancelled.
*
* @type number
*/
this.touchBoundary = options.touchBoundary ||
10;
/**
* The FastClick layer.
*
* @type Element
*/
this.layer = layer;
/**
* The minimum time between tap(touchstart and touchend) events
*
* @type number
*/
this.tapDelay = options.tapDelay ||
200;
/**
* The maximum time for a tap
*
* @type number
*/
this.tapTimeout = options.tapTimeout ||
700;
if (FastClick.notNeeded(layer)) {
return;
}
function bind(method, context) {
return function() {
return method.apply(context, arguments); };
}
var methods = [
'onMouse',
'onClick',
'onTouchStart',
'onTouchMove',
'onTouchEnd',
'onTouchCancel'];
var context =
this;
for (
var i =
0, l = methods.length; i < l; i++) {
context[methods[i]] = bind(context[methods[i]], context);
}
if (deviceIsAndroid) {
layer.addEventListener(
'mouseover',
this.onMouse,
true);
layer.addEventListener(
'mousedown',
this.onMouse,
true);
layer.addEventListener(
'mouseup',
this.onMouse,
true);
}
layer.addEventListener(
'click',
this.onClick,
true);
layer.addEventListener(
'touchstart',
this.onTouchStart,
false);
layer.addEventListener(
'touchmove',
this.onTouchMove,
false);
layer.addEventListener(
'touchend',
this.onTouchEnd,
false);
layer.addEventListener(
'touchcancel',
this.onTouchCancel,
false);
if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(
type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (
type ===
'click') {
rmv.call(layer,
type, callback.hijacked || callback, capture);
}
else {
rmv.call(layer,
type, callback, capture);
}
};
layer.addEventListener = function(
type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (
type ===
'click') {
adv.call(layer,
type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
}
else {
adv.call(layer,
type, callback, capture);
}
};
}
if (typeof layer.onclick ===
'function') {
oldOnClick = layer.onclick;
layer.addEventListener(
'click', function(event) {
oldOnClick(event);
},
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 && !deviceIsWindowsPhone;
/**
* iOS requires exceptions.
*
* @type boolean
*/
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);
/**
* iOS 6.0-7.* requires the target element to be manually derived
*
* @type boolean
*/
var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [
6-
7]_\d/).test(navigator.userAgent);
/**
* BlackBerry requires exceptions.
*
* @type boolean
*/
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) {
switch (target.nodeName.toLowerCase()) {
case 'button':
case 'select':
case 'textarea':
if (target.disabled) {
return true;
}
break;
case 'input':
if ((deviceIsIOS && target.
type ===
'file') || target.disabled) {
return true;
}
break;
case 'label':
case 'iframe':
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) {
switch (target.nodeName.toLowerCase()) {
case 'textarea':
return true;
case 'select':
return !deviceIsAndroid;
case 'input':
switch (target.
type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
return false;
}
return !target.disabled && !target.readOnly;
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) {
var clickEvent, touch;
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[
0];
clickEvent = document.createEvent(
'MouseEvents');
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) {
if (deviceIsAndroid && targetElement.tagName.toLowerCase() ===
'select') {
return 'mousedown';
}
return 'click';
};
/**
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.focus = function(targetElement) {
var length;
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) {
var scrollParent, parentElement;
scrollParent = targetElement.fastClickScrollParent;
if (!scrollParent || !scrollParent.contains(targetElement)) {
parentElement = targetElement;
do {
if (parentElement.scrollHeight > parentElement.offsetHeight) {
scrollParent = parentElement;
targetElement.fastClickScrollParent = parentElement;
break;
}
parentElement = parentElement.parentElement;
}
while (parentElement);
}
if (scrollParent) {
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
}
};
/**
* @param {EventTarget} targetElement
* @returns {Element|EventTarget}
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
if (eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}
return eventTarget;
};
/**
* On touch start, record the position and scroll offset.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchStart = function(event) {
var targetElement, touch, selection;
if (event.targetTouches.length >
1) {
return true;
}
targetElement =
this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[
0];
if (deviceIsIOS) {
selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {
return true;
}
if (!deviceIsIOS4) {
if (touch.identifier && touch.identifier ===
this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
this.lastTouchIdentifier = touch.identifier;
this.updateScrollParent(targetElement);
}
}
this.trackingClick =
true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
if ((event.timeStamp -
this.lastClickTime) <
this.tapDelay) {
event.preventDefault();
}
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) {
var touch = event.changedTouches[
0], boundary =
this.touchBoundary;
if (Math.abs(touch.pageX -
this.touchStartX) > boundary || Math.abs(touch.pageY -
this.touchStartY) > boundary) {
return true;
}
return false;
};
/**
* Update the last position.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchMove = function(event) {
if (!
this.trackingClick) {
return true;
}
if (
this.targetElement !==
this.getTargetElementFromEventTarget(event.target) ||
this.touchHasMoved(event)) {
this.trackingClick =
false;
this.targetElement =
null;
}
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) {
if (labelElement.control !== undefined) {
return labelElement.control;
}
if (labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}
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) {
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement =
this.targetElement;
if (!
this.trackingClick) {
return true;
}
if ((event.timeStamp -
this.lastClickTime) <
this.tapDelay) {
this.cancelNextClick =
true;
return true;
}
if ((event.timeStamp -
this.trackingClickStart) >
this.tapTimeout) {
return true;
}
this.cancelNextClick =
false;
this.lastClickTime = event.timeStamp;
trackingClickStart =
this.trackingClickStart;
this.trackingClick =
false;
this.trackingClickStart =
0;
if (deviceIsIOSWithBadTarget) {
touch = event.changedTouches[
0];
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
targetElement.fastClickScrollParent =
this.targetElement.fastClickScrollParent;
}
targetTagName = targetElement.tagName.toLowerCase();
if (targetTagName ===
'label') {
forElement =
this.findControl(targetElement);
if (forElement) {
this.focus(targetElement);
if (deviceIsAndroid) {
return false;
}
targetElement = forElement;
}
}
else if (
this.needsFocus(targetElement)) {
if ((event.timeStamp - trackingClickStart) >
100 || (deviceIsIOS && window.top !== window && targetTagName ===
'input')) {
this.targetElement =
null;
return false;
}
this.focus(targetElement);
this.sendClick(targetElement, event);
if (!deviceIsIOS || targetTagName !==
'select') {
this.targetElement =
null;
event.preventDefault();
}
return false;
}
if (deviceIsIOS && !deviceIsIOS4) {
scrollParent = targetElement.fastClickScrollParent;
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
return true;
}
}
if (!
this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
return false;
};
/**
* On touch cancel, stop tracking the click.
*
* @returns {void}
*/
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) {
if (!
this.targetElement) {
return true;
}
if (event.forwardedTouchEvent) {
return true;
}
if (!event.cancelable) {
return true;
}
if (!
this.needsClick(
this.targetElement) ||
this.cancelNextClick) {
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
}
else {
event.propagationStopped =
true;
}
event.stopPropagation();
event.preventDefault();
return false;
}
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.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onClick = function(event) {
var permitted;
if (
this.trackingClick) {
this.targetElement =
null;
this.trackingClick =
false;
return true;
}
if (event.target.
type ===
'submit' && event.detail ===
0) {
return true;
}
permitted =
this.onMouse(event);
if (!permitted) {
this.targetElement =
null;
}
return permitted;
};
/**
* Remove all FastClick's event listeners.
*
* @returns {void}
*/
FastClick.prototype.destroy = function() {
var layer =
this.layer;
if (deviceIsAndroid) {
layer.removeEventListener(
'mouseover',
this.onMouse,
true);
layer.removeEventListener(
'mousedown',
this.onMouse,
true);
layer.removeEventListener(
'mouseup',
this.onMouse,
true);
}
layer.removeEventListener(
'click',
this.onClick,
true);
layer.removeEventListener(
'touchstart',
this.onTouchStart,
false);
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) {
var metaViewport;
var chromeVersion;
var blackberryVersion;
var firefoxVersion;
if (typeof window.ontouchstart ===
'undefined') {
return true;
}
chromeVersion = +(/Chrome\/([
0-
9]+)/.exec(navigator.userAgent) || [,
0])[
1];
if (chromeVersion) {
if (deviceIsAndroid) {
metaViewport = document.querySelector(
'meta[name=viewport]');
if (metaViewport) {
if (metaViewport.content.indexOf(
'user-scalable=no') !== -
1) {
return true;
}
if (chromeVersion >
31 && document.documentElement.scrollWidth <= window.outerWidth) {
return true;
}
}
}
else {
return true;
}
}
if (deviceIsBlackBerry10) {
blackberryVersion = navigator.userAgent.
match(/Version\/([
0-
9]*)\.([
0-
9]*)/);
if (blackberryVersion[
1] >=
10 && blackberryVersion[
2] >=
3) {
metaViewport = document.querySelector(
'meta[name=viewport]');
if (metaViewport) {
if (metaViewport.content.indexOf(
'user-scalable=no') !== -
1) {
return true;
}
if (document.documentElement.scrollWidth <= window.outerWidth) {
return true;
}
}
}
}
if (layer.style.msTouchAction ===
'none' || layer.style.touchAction ===
'manipulation') {
return true;
}
firefoxVersion = +(/Firefox\/([
0-
9]+)/.exec(navigator.userAgent) || [,
0])[
1];
if (firefoxVersion >=
27) {
metaViewport = document.querySelector(
'meta[name=viewport]');
if (metaViewport && (metaViewport.content.indexOf(
'user-scalable=no') !== -
1 || document.documentElement.scrollWidth <= window.outerWidth)) {
return true;
}
}
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
*/
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};
if (typeof define ===
'function' && typeof define.amd ===
'object' && define.amd) {
define(function() {
return FastClick;
});
}
else if (typeof module !==
'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
}
else {
window.FastClick = FastClick;
}
}());
开始调用
FastClick.attach(document.
body);