;
(function() {
'use strict';
//建構函式
function FastClick(layer, options) {
var oldOnClick;
options = options || {};
//是否開始追蹤click事件
this.trackingClick = false;
//儲存第一次按下時間戳
this.trackingClickStart = 0;
//目標元素
this.targetElement = null;
//存放座標值X
this.touchStartX = 0;
//存放座標值Y
this.touchStartY = 0;
//主要hack iOS4下的一個怪異問題
this.lastTouchIdentifier = 0;
//用於區分是click還是Touchmove,若出點移動超過該值則視為touchmove
this.touchBoundary = options.touchBoundary || 10;
// 繫結了FastClick的元素,一般是是body
this.layer = layer;
//雙擊最小點選時間差
this.tapDelay = options.tapDelay || 200;
//長按最大時間
this.tapTimeout = options.tapTimeout || 700;
//如果是屬於不需要處理的元素型別,則直接返回
if(FastClick.notNeeded(layer)) {
return;
}
//語法糖,相容一些用不了 Function.prototype.bind 的舊安卓
//所以後面不走 layer.addEventListener('click', this.onClick.bind(this), true);
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);
// 相容不支援 stopImmediatePropagation 的瀏覽器(比如 Android 2)
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') {
//留意這裡 callback.hijacked 中會判斷 event.propagationStopped 是否為真來確保(安卓的onMouse事件)只執行一次
//在 onMouse 事件裡會給 event.propagationStopped 賦值 true
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if(!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}
// 如果layer直接在DOM上寫了 onclick 方法,那我們需要把它替換為 addEventListener 繫結形式
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;
//判斷元素是否要保留穿透功能
FastClick.prototype.needsClick = function(target) {
switch(target.nodeName.toLowerCase()) {
// disabled的input
case 'button':
case 'select':
case 'textarea':
if(target.disabled) {
return true;
}
break;
case 'input':
// file元件必須通過原生click事件點選才有效
if((deviceIsIOS && target.type === 'file') || target.disabled) {
return true;
}
break;
case 'label':
case 'iframe':
case 'video':
return true;
}
//元素帶了名為“bneedsclick”的class也返回true
return(/\bneedsclick\b/).test(target.className);
};
//判斷給定元素是否需要通過合成click事件來模擬聚焦
FastClick.prototype.needsFocus = function(target) {
switch(target.nodeName.toLowerCase()) {
case 'textarea':
return true;
case 'select':
return !deviceIsAndroid; //iOS下的select得走穿透點選才行
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:
//帶有名為“bneedsfocus”的class則返回true
return(/\bneedsfocus\b/).test(target.className);
}
};
//合成一個click事件並在指定元素上觸發
FastClick.prototype.sendClick = function(targetElement, event) {
var clickEvent, touch;
// 在一些安卓機器中,得讓頁面所存在的 activeElement(聚焦的元素,比如input)失焦,否則合成的click事件將無效
if(document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[0];
// 合成(Synthesise) 一個 click 事件
// 通過一個額外屬性確保它能被追蹤(tracked)
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; // fastclick的內部變數,用來識別click事件是原生還是合成的
targetElement.dispatchEvent(clickEvent); //立即觸發其click事件
};
FastClick.prototype.determineEventType = function(targetElement) {
//安卓裝置下 Select 無法通過合成的 click 事件被展開,得改為 mousedown
if(deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
return 'mousedown';
}
return 'click';
};
//設定元素聚焦事件
FastClick.prototype.focus = function(targetElement) {
var length;
// 元件建議通過setSelectionRange(selectionStart, selectionEnd)來設定游標範圍(注意這樣還沒有聚焦
// 要等到後面觸發 sendClick 事件才會聚焦)
// 另外 iOS7 下有些input元素(比如 date datetime month) 的 selectionStart 和 selectionEnd 特性是沒有整型值的,
// 導致會丟擲一個關於 setSelectionRange 的模糊錯誤,它們需要改用 focus 事件觸發
if(deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
length = targetElement.value.length;
targetElement.setSelectionRange(length, length);
} else {
//直接觸發其focus事件
targetElement.focus();
}
};
/**
* 檢查target是否一個滾動容器裡的子元素,如果是則給它加個標記
*/
FastClick.prototype.updateScrollParent = function(targetElement) {
var scrollParent, parentElement;
scrollParent = targetElement.fastClickScrollParent;
// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
// target element was moved to another parent.
if(!scrollParent || !scrollParent.contains(targetElement)) {
parentElement = targetElement;
do {
if(parentElement.scrollHeight > parentElement.offsetHeight) {
scrollParent = parentElement;
targetElement.fastClickScrollParent = parentElement;
break;
}
parentElement = parentElement.parentElement;
} while (parentElement);
}
// 給滾動容器加個標誌fastClickLastScrollTop,值為其當前垂直滾動偏移
if(scrollParent) {
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
}
};
/**
* 返回目標元素
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
// 一些較老的瀏覽器,target 可能會是一個文字節點,得返回其DOM節點
if(eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}
return eventTarget;
};
FastClick.prototype.onTouchStart = function(event) {
var targetElement, touch, selection;
// 多指觸控的手勢則忽略
if(event.targetTouches.length > 1) {
return true;
}
targetElement = this.getTargetElementFromEventTarget(event.target); //一些較老的瀏覽器,target 可能會是一個文字節點,得返回其DOM節點
touch = event.targetTouches[0];
if(deviceIsIOS) { //IOS處理
// 若使用者已經選中了一些內容(比如選中了一段文字打算複製),則忽略
selection = window.getSelection();
if(selection.rangeCount && !selection.isCollapsed) {
return true;
}
if(!deviceIsIOS4) { //是否IOS4
//怪異特性處理——若click事件回撥開啟了一個alert/confirm,使用者下一次tap頁面的其它地方時,新的touchstart和touchend
//事件會擁有同一個touch.identifier(新的 touch event 會跟上一次觸發alert點選的 touch event 一樣),
//為避免將新的event當作之前的event導致問題,這裡需要禁用預設事件
//另外chrome的開發工具啟用'Emulate touch events'後,iOS UA下的 identifier 會變成0,所以要做容錯避免除錯過程也被禁用事件了
if(touch.identifier && touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
this.lastTouchIdentifier = touch.identifier;
// 如果target是一個滾動容器裡的一個子元素(使用了 -webkit-overflow-scrolling: touch) ,而且滿足:
// 1) 使用者非常快速地滾動外層滾動容器
// 2) 使用者通過tap停止住了這個快速滾動
// 這時候最後的'touchend'的event.target會變成使用者最終手指下的那個元素
// 所以當快速滾動開始的時候,需要做檢查target是否滾動容器的子元素,如果是,做個標記
// 在touchend時檢查這個標記的值(滾動容器的scrolltop)是否改變了,如果是則說明頁面在滾動中,需要取消fastclick處理
this.updateScrollParent(targetElement);
}
}
this.trackingClick = true; //做個標誌表示開始追蹤click事件了
this.trackingClickStart = event.timeStamp; //標記下touch事件開始的時間戳
this.targetElement = targetElement;
//標記touch起始點的頁面偏移值
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
// this.lastClickTime 是在 touchend 裡標記的事件時間戳
// this.tapDelay 為常量 200 (ms)
// 此舉用來避免 phantom 的雙擊(200ms內快速點了兩次)觸發 click
// 反正200ms內的第二次點選會禁止觸發點選的預設事件
if((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
return true;
};
//判斷是否移動了
//this.touchBoundary是常量,值為10
//如果touch已經移動了10個偏移量單位,則應當作為移動事件處理而非click事件
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;
};
FastClick.prototype.onTouchMove = function(event) {
//不是需要被追蹤click的事件則忽略
if(!this.trackingClick) {
return true;
}
// 如果target突然改變了,或者使用者其實是在移動手勢而非想要click
// 則應該清掉this.trackingClick和this.targetElement,告訴後面的事件你們也不用處理了
if(this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
}
return true;
};
//找到label標籤所對映的元件,方便讓使用者點label的時候直接啟用該元件
FastClick.prototype.findControl = function(labelElement) {
// 有快取則直接讀快取著的
if(labelElement.control !== undefined) {
return labelElement.control;
}
// 獲取指向的元件
if(labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}
// 沒有for屬性則啟用頁面第一個元件(labellable 元素)
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};
FastClick.prototype.onTouchEnd = function(event) {
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
if(!this.trackingClick) {
return true;
}
// 避免 phantom 的雙擊(200ms內快速點了兩次)觸發 click
// 我們在 ontouchstart 裡已經做過一次判斷了(僅僅禁用預設事件),這裡再做一次判斷
if((event.timeStamp - this.lastClickTime) < this.tapDelay) {
this.cancelNextClick = true; //該屬性會在 onMouse 事件中被判斷,為true則徹底禁用事件和冒泡
return true;
}
//this.tapTimeout是常量,值為700
//識別是否為長按事件,如果是(大於700ms)則忽略
if((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
}
// 得重置為false,避免input事件被意外取消
// 例子見 https://github.com/ftlabs/fastclick/issues/156
this.cancelNextClick = false;
this.lastClickTime = event.timeStamp; //標記touchend時間,方便下一次的touchstart做雙擊校驗
trackingClickStart = this.trackingClickStart;
//重置 this.trackingClick 和 this.trackingClickStart
this.trackingClick = false;
this.trackingClickStart = 0;
// iOS 6.0-7.*版本下有個問題 —— 如果layer處於transition或scroll過程,event所提供的target是不正確的
// 所以我們們得重找 targetElement(這裡通過 document.elementFromPoint 介面來尋找)
if(deviceIsIOSWithBadTarget) { //iOS 6.0-7.*版本
touch = event.changedTouches[0]; //手指離開前的觸點
// 有些情況下 elementFromPoint 裡的引數是預期外/不可用的, 所以還得避免 targetElement 為 null
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
// target可能不正確需要重找,但fastClickScrollParent是不會變的
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
}
targetTagName = targetElement.tagName.toLowerCase();
if(targetTagName === 'label') { //是label則啟用其指向的元件
forElement = this.findControl(targetElement);
if(forElement) {
this.focus(targetElement);
//安卓直接返回(無需合成click事件觸發,因為點選和啟用元素不同,不存在點透)
if(deviceIsAndroid) {
return false;
}
targetElement = forElement;
}
} else if(this.needsFocus(targetElement)) { //非label則識別是否需要focus的元素
//手勢停留在元件元素時長超過100ms,則置空this.targetElement並返回
//(而不是通過呼叫this.focus來觸發其聚焦事件,走的原生的click/focus事件觸發流程)
//這也是為何文章開頭提到的問題中,稍微久按一點(超過100ms)textarea是可以把游標定位在正確的地方的原因
//另外iOS下有個意料之外的bug——如果被點選的元素所在文件是在iframe中的,手動呼叫其focus的話,
//會發現你往其中輸入的text是看不到的(即使value做了更新),so這裡也直接返回
if((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
this.targetElement = null;
return false;
}
this.focus(targetElement);
this.sendClick(targetElement, event); //立即觸發其click事件,而無須等待300ms
//iOS4下的 select 元素不能禁用預設事件(要確保它能被穿透),否則不會開啟select目錄
//有時候 iOS6/7 下(VoiceOver開啟的情況下)也會如此
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;
}
}
// 檢視元素是否無需處理的白名單內(比如加了名為“needsclick”的class)
// 不是白名單的則照舊預防穿透處理,立即觸發合成的click事件
if(!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
return false;
};
FastClick.prototype.onTouchCancel = function() {
this.trackingClick = false;
this.targetElement = null;
};
//用於決定是否允許穿透事件(觸發layer的click預設事件)
FastClick.prototype.onMouse = function(event) {
// touch事件一直沒觸發
if(!this.targetElement) {
return true;
}
if(event.forwardedTouchEvent) { //觸發的click事件是合成的
return true;
}
// 程式設計派生的事件所對應元素事件可以被允許
// 確保其沒執行過 preventDefault 方法(event.cancelable 不為 true)即可
if(!event.cancelable) {
return true;
}
// 需要做預防穿透處理的元素,或者做了快速(200ms)雙擊的情況
if(!this.needsClick(this.targetElement) || this.cancelNextClick) {
//停止當前預設事件和冒泡
if(event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else {
// 不支援 stopImmediatePropagation 的裝置(比如Android 2)做標記,
// 確保該事件回撥不會執行(見126行)
event.propagationStopped = true;
}
// 取消事件和冒泡
event.stopPropagation();
event.preventDefault();
return false;
}
//允許穿透
return true;
};
//click事件常規都是touch事件衍生來的,也排在touch後面觸發。
//對於那些我們在touch事件過程沒有禁用掉預設事件的event來說,我們還需要在click的捕獲階段進一步
//做判斷決定是否要禁掉點選事件(防穿透)
FastClick.prototype.onClick = function(event) {
var permitted;
// 如果還有 trackingClick 存在,可能是某些UI事件阻塞了touchEnd 的執行
if(this.trackingClick) {
this.targetElement = null;
this.trackingClick = false;
return true;
}
// 依舊是對 iOS 怪異行為的處理 —— 如果使用者點選了iOS模擬器裡某個表單中的一個submit元素
// 或者點選了彈出來的鍵盤裡的“Go”按鈕,會觸發一個“偽”click事件(target是一個submit-type的input元素)
if(event.target.type === 'submit' && event.detail === 0) {
return true;
}
permitted = this.onMouse(event);
if(!permitted) { //如果點選是被允許的,將this.targetElement置空可以確保onMouse事件裡不會阻止預設事件
this.targetElement = null;
}
//沒有多大意義
return permitted;
};
//銷燬Fastclick所註冊的監聽事件。是給外部例項去呼叫的
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);
};
//是否沒必要使用到 Fastclick 的檢測
FastClick.notNeeded = function(layer) {
var metaViewport;
var chromeVersion;
var blackberryVersion;
var firefoxVersion;
// 不支援觸控的裝置
if(typeof window.ontouchstart === 'undefined') {
return true;
}
// 獲取Chrome版本號,若非Chrome則返回0
chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [, 0])[1];
if(chromeVersion) {
if(deviceIsAndroid) { //安卓
metaViewport = document.querySelector('meta[name=viewport]');
if(metaViewport) {
// 安卓下,帶有 user-scalable="no" 的 meta 標籤的 chrome 是會自動禁用 300ms 延遲的,所以無需 Fastclick
if(metaViewport.content.indexOf('user-scalable=no') !== -1) {
return true;
}
// 安卓Chrome 32 及以上版本,若帶有 width=device-width 的 meta 標籤也是無需 FastClick 的
if(chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
return true;
}
}
// 其它的就肯定是桌面級的 Chrome 了,更不需要 FastClick 啦
} 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;
}
}
}
}
// 帶有 -ms-touch-action: none / manipulation 特性的 IE10 會禁用雙擊放大,也沒有 300ms 時延
if(layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
return true;
}
// Firefox檢測,同上
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;
}
}
// IE11 推薦使用沒有“-ms-”字首的 touch-action 樣式特性名
if(layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
return true;
}
return false;
};
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};
if(typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
return FastClick;
});
} else if(typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
window.FastClick = FastClick;
}
}());
複製程式碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Demo</title>
<script src="./fastclick.js"></script>
<style>
div {
width: 200px;
background: red;Y
margin: 0 auto;
height: 200px;
color: wheat;
font-size: 25px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="main">FastClick</div>
<script>
FastClick.attach(document.body);
document.getElementById("main").addEventListener("click", function(event) {
console.log(event.target.innerText)
}, false)
</script>
</body>
</html>
複製程式碼