zoukankan      html  css  js  c++  java
  • fastclick源码分析

    在分析fastclcik源码之前需要先搞清楚为什么非得用click代替touchstart,移动端直接使用touchstart不就行了吗。我认为主要有以下两大理由:

    1、部分网站PC端、移动端共用一套代码,都绑定了touchstart,PC端还怎么玩

    2、二者触发条件不同:a)touchstart 手指触摸到显示屏即触发事件 b)click 手指触摸到显示屏,未曾在屏幕上移动(或移动一个非常小的位移值),然后手指离开屏幕,从触摸到离开时间间隔较短,此时才会触发click事件。

    click体验要明显好于touchstart,故我们要为click填坑。

    简单模拟

     经过一段时间修改测试写下如下代码,运行效果还行:

     1 <!doctype html>
     2 <html>
     3 <head>
     4     <meta charset="utf-8">
     5     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimun-scale=1.0">
     6     <title>手机端点击</title>
     7 </head>
     8 <body>
     9 <div id="demo1" style="100px;height:100px;background:red;"></div>
    10 <div id="demo2" style="100px;height:100px;background:blue;"></div>
    11 <script>
    12     var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2")
    13     demo1.addEventListener('touchstart', function(){
    14         demo1.innerHTML=demo1.innerHTML + "<br>touchstart"
    15     })
    16     demo2.addEventListener('click', function (e) {
    17         if(!e.ming){
    18             e.preventDefault();
    19             return
    20         }
    21         demo2.innerHTML=demo2.innerHTML + "<br>click"
    22     })
    23 
    24     var el
    25     document.addEventListener("touchstart", function(e){
    26         el = e.target
    27     })
    28     document.addEventListener("touchend", function(e){
    29         var event = document.createEvent("MouseEvents")
    30         event.initEvent("click", true, true)
    31         event.ming = true
    32         el && el.dispatchEvent(event)
    33     })
    34 </script>
    35 </body>
    36 </html>
    View Code

    在用我的IOS9浏览器测试时,效果还行。就是点击demo2时会有闪烁发生,并有一个黑框。经过测试,黑框是outline,闪烁是click触发了浏览器的重绘。outline设为none即可,闪烁暂时没找到方法避免,这不是本文重点,以后再研究。

    点透问题

    大家喜欢用fastclick还有个原因是它可以避免点透,现在先看看我们的代码能不能避免点透,先搞个例子:

    <!doctype html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimun-scale=1.0">
        <title>手机端点击</title>
    <style>
    body{margin:0;}
    input{width:100%;height:20px;}
    #demo1{padding-top:20px;}
    #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
    #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
    .hide{display:none;}
    </style>
    </head>
    <body>
    <div id="demo1">
        <input id="text">
    </div>
    <div id="demo2">
        <button id="btn">点击我</button>
    </div>
    <script>
        var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn")
    
        btn.addEventListener('click', function(e){
            if(!e.ming){
                e.preventDefault();
                e.stopPropagation();
                return
            }
            demo2.className = "hide";
        })
    
        var el
        document.addEventListener("touchstart", function(e){
            el = e.target
        })
        document.addEventListener("touchend", function(e){
            var event = document.createEvent("MouseEvents")
            event.initEvent("click", true, true)
            event.ming = true
            el && el.dispatchEvent(event)
        })
    </script>
    </body>
    </html>
    View Code

    执行代码,顺利点透

     将touchend默认事件阻止即可,这时就不会再出现点透问题,但悲剧的是input永远也获取不到焦点了

    <!doctype html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <title>手机端点击</title>
    <style>
    body{margin:0;}
    input{width:90%;height:20px;}
    #demo1{padding-top:20px;}
    #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
    #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
    .hide{display:none;}
    </style>
    </head>
    <body>
    <div id="demo1">
        <input id="text">
    </div>
    <div id="demo2">
        <button id="btn">点击我</button>
    </div>
    <script>
        var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn")
    
        document.addEventListener('click', function(e){
            if(e.ming){
                return true;
            }
            if (e.stopImmediatePropagation) {
                e.stopImmediatePropagation();
            } else {
                e.propagationStopped = true;
            }
            e.stopPropagation();
            e.preventDefault();
            return true;
        }, true)
    
        btn.addEventListener('click', function(e){
            demo2.className = "hide";
        })
    
        var el
        document.addEventListener("touchstart", function(e){
            el = e.target
        })
        document.addEventListener("touchend", function(e){
            e.preventDefault();
            var event = document.createEvent("MouseEvents")
            event.initEvent("click", true, true)
            event.ming = true
            el && el.dispatchEvent(event)
        })
    </script>
    </body>
    </html>
    View Code

    此处分析:我们知道点击后事件顺序如下:touchstart、touchend、click,touchend触发后悬浮框demo2隐藏,浏览器自带的click被阻止了默认操作,怎么还会点透呢。测试如下:

    <!doctype html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <title>手机端点击</title>
    <style>
    body{margin:0;}
    input{width:90%;height:20px;}
    #demo1{padding-top:20px;}
    #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
    #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
    .hide{display:none;}
    </style>
    </head>
    <body>
    <div id="demo1">
        <input id="text">
    </div>
    <div id="demo2">
        <button id="btn">点击我</button>
    </div>
    <script>
        var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn"), text = document.querySelector("#text")
    
        document.addEventListener('click', function(e){
            if(e.ming){
                return true;
            }
            if (e.stopImmediatePropagation) {
                e.stopImmediatePropagation();
            } else {
                e.propagationStopped = true;
            }
            e.stopPropagation();
            e.preventDefault();
            return true;
        }, true)
        /*document.addEventListener('mousedown', function(e){
            if(e.ming){
                return true;
            }
            if (e.stopImmediatePropagation) {
                e.stopImmediatePropagation();
            } else {
                e.propagationStopped = true;
            }
            e.stopPropagation();
            e.preventDefault();
            return true;
        }, true)*/
    
        text.addEventListener("click", function(){
            console.log("text click")
        })
    
        text.addEventListener("touchend", function(){
            console.log("text touchend")
        })
    
        text.addEventListener("touchstart", function(){
            console.log("text touchstart")
        })
        text.addEventListener("mousedown", function(){
            console.log("text mousedown")
        })
    
        btn.addEventListener('click', function(e){
            console.log(e.ming);
            demo2.className = "hide";
        })
    
        var el
        document.addEventListener("touchstart", function(e){
            el = e.target
        })
        document.addEventListener("touchend", function(e){
            console.log('touchend')
            var event = document.createEvent("MouseEvents")
            event.initEvent("click", true, true)
            event.ming = true
            el && el.dispatchEvent(event)
        })
    </script>
    </body>
    </html>
    View Code

    结果发现input上只有mousedown被触发了,原来是mousedown搞的事,手机点击后触发事件正确顺序是:touchstart、touchend、click、mousedown。该怎么阻止mosedown呢。

    这里研究一下阻止哪些事件后input无法获取焦点

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <style type="text/css">
            .bt { position: absolute; top: 50px; display: block; height: 50px; }
        </style>
    </head>
    <body>
    <input id="input1">
    <input id="input2">
    <input id="input3">
    <input id="input4">
    <input id="input5">
    <input id="input6">
    </body>
    <script type="text/javascript">
        var input1 = document.querySelector('#input1'), input2 = document.querySelector('#input2'), input3 = document.querySelector('#input3')
                ,input4 = document.querySelector('#input4'), input5 = document.querySelector('#input5'),input6 = document.querySelector('#input6')
    
        input1.addEventListener('touchstart', function(e){
            e.preventDefault();
        })
        input2.addEventListener('touchend', function(e){
            e.preventDefault();
        })
        input3.addEventListener('click', function(e){
            e.preventDefault();
        })
        input4.addEventListener('mousedown', function(e){
            e.preventDefault();
        })
        input5.addEventListener('mouseout', function(e){
            e.preventDefault();
        })
        input6.addEventListener('mouseenter', function(e){
            e.preventDefault();
        })
    </script>
    </html>
    View Code

    手机测试发现,touchstart、touchend、mousedown事件被阻止后,input就无法再获取焦点。而阻止click事件并不阻止获取焦点。ok,来看看fastclick的解决方案。

    fastclick解决方案

      1 ;(function () {
      2     'use strict';
      3 
      4     /**
      5      * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
      6      *
      7      * @codingstandard ftlabs-jsv2
      8      * @copyright The Financial Times Limited [All Rights Reserved]
      9      * @license MIT License (see LICENSE.txt)
     10      */
     11 
     12     /*jslint browser:true, node:true*/
     13     /*global define, Event, Node*/
     14 
     15 
     16     /**
     17      * Instantiate fast-clicking listeners on the specified layer.
     18      *
     19      * @constructor
     20      * @param {Element} layer The layer to listen on
     21      * @param {Object} [options={}] The options to override the defaults
     22      */
     23     function FastClick(layer, options) {
     24         var oldOnClick;
     25 
     26         options = options || {};
     27 
     28         /**
     29          * Whether a click is currently being tracked.
     30          *
     31          * @type boolean
     32          */
     33         this.trackingClick = false;
     34 
     35 
     36         /**
     37          * Timestamp for when click tracking started.
     38          *
     39          * @type number
     40          */
     41         this.trackingClickStart = 0;
     42 
     43 
     44         /**
     45          * The element being tracked for a click.
     46          *
     47          * @type EventTarget
     48          */
     49         this.targetElement = null;
     50 
     51 
     52         /**
     53          * X-coordinate of touch start event.
     54          *
     55          * @type number
     56          */
     57         this.touchStartX = 0;
     58 
     59 
     60         /**
     61          * Y-coordinate of touch start event.
     62          *
     63          * @type number
     64          */
     65         this.touchStartY = 0;
     66 
     67 
     68         /**
     69          * ID of the last touch, retrieved from Touch.identifier.
     70          *
     71          * @type number
     72          */
     73         this.lastTouchIdentifier = 0;
     74 
     75 
     76         /**
     77          * Touchmove boundary, beyond which a click will be cancelled.
     78          *
     79          * @type number
     80          */
     81         this.touchBoundary = options.touchBoundary || 10;
     82 
     83 
     84         /**
     85          * The FastClick layer.
     86          *
     87          * @type Element
     88          */
     89         this.layer = layer;
     90 
     91         /**
     92          * The minimum time between tap(touchstart and touchend) events
     93          *
     94          * @type number
     95          */
     96         this.tapDelay = options.tapDelay || 200;
     97 
     98         /**
     99          * The maximum time for a tap
    100          *
    101          * @type number
    102          */
    103         this.tapTimeout = options.tapTimeout || 700;
    104 
    105     //部分浏览器click已经不会延迟300ms 不需要使用fastclick了
    106         if (FastClick.notNeeded(layer)) {
    107             return;
    108         }
    109 
    110         // Some old versions of Android don't have Function.prototype.bind
    111         function bind(method, context) {
    112             return function() { return method.apply(context, arguments); };
    113         }
    114 
    115     //需要监听的事件
    116         var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
    117         var context = this;
    118         for (var i = 0, l = methods.length; i < l; i++) {
    119             context[methods[i]] = bind(context[methods[i]], context);
    120         }
    121 
    122     //android除了touch事件,还需要监视mouse事件
    123         // Set up event handlers as required
    124         if (deviceIsAndroid) {
    125             layer.addEventListener('mouseover', this.onMouse, true);
    126             layer.addEventListener('mousedown', this.onMouse, true);
    127             layer.addEventListener('mouseup', this.onMouse, true);
    128         }
    129 
    130         layer.addEventListener('click', this.onClick, true);
    131         layer.addEventListener('touchstart', this.onTouchStart, false);
    132         layer.addEventListener('touchmove', this.onTouchMove, false);
    133         layer.addEventListener('touchend', this.onTouchEnd, false);
    134         layer.addEventListener('touchcancel', this.onTouchCancel, false);
    135 
    136         // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
    137         // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
    138         // layer when they are cancelled.
    139         if (!Event.prototype.stopImmediatePropagation) {
    140             layer.removeEventListener = function(type, callback, capture) {
    141                 var rmv = Node.prototype.removeEventListener;
    142                 if (type === 'click') {
    143                     rmv.call(layer, type, callback.hijacked || callback, capture);
    144                 } else {
    145                     rmv.call(layer, type, callback, capture);
    146                 }
    147             };
    148 
    149             layer.addEventListener = function(type, callback, capture) {
    150                 var adv = Node.prototype.addEventListener;
    151                 if (type === 'click') {
    152                     adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
    153                         if (!event.propagationStopped) {
    154                             callback(event);
    155                         }
    156                     }), capture);
    157                 } else {
    158                     adv.call(layer, type, callback, capture);
    159                 }
    160             };
    161         }
    162 
    163         // If a handler is already declared in the element's onclick attribute, it will be fired before
    164         // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
    165         // adding it as listener.
    166         if (typeof layer.onclick === 'function') {
    167 
    168             // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
    169             // - the old one won't work if passed to addEventListener directly.
    170             oldOnClick = layer.onclick;
    171             layer.addEventListener('click', function(event) {
    172                 oldOnClick(event);
    173             }, false);
    174             layer.onclick = null;
    175         }
    176     }
    177 
    178     /**
    179     * Windows Phone 8.1 fakes user agent string to look like Android and iPhone.
    180     *
    181     * @type boolean
    182     */
    183     var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0;
    184 
    185     /**
    186      * Android requires exceptions.
    187      *
    188      * @type boolean
    189      */
    190     var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone;
    191 
    192 
    193     /**
    194      * iOS requires exceptions.
    195      *
    196      * @type boolean
    197      */
    198     var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone;
    199 
    200 
    201     /**
    202      * iOS 4 requires an exception for select elements.
    203      *
    204      * @type boolean
    205      */
    206     var deviceIsIOS4 = deviceIsIOS && (/OS 4_d(_d)?/).test(navigator.userAgent);
    207 
    208 
    209     /**
    210      * iOS 6.0-7.* requires the target element to be manually derived
    211      *
    212      * @type boolean
    213      */
    214     var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_d/).test(navigator.userAgent);
    215 
    216     /**
    217      * BlackBerry requires exceptions.
    218      *
    219      * @type boolean
    220      */
    221     var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
    222 
    223     /**
    224      * Determine whether a given element requires a native click.
    225      *
    226      * @param {EventTarget|Element} target Target DOM element
    227      * @returns {boolean} Returns true if the element needs a native click
    228      */
    229     FastClick.prototype.needsClick = function(target) {
    230         switch (target.nodeName.toLowerCase()) {
    231 
    232         // Don't send a synthetic click to disabled inputs (issue #62)
    233         case 'button':
    234         case 'select':
    235         case 'textarea':
    236             if (target.disabled) {
    237                 return true;
    238             }
    239 
    240             break;
    241         case 'input':
    242 
    243             // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
    244             if ((deviceIsIOS && target.type === 'file') || target.disabled) {
    245                 return true;
    246             }
    247 
    248             break;
    249         case 'label':
    250         case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames
    251         case 'video':
    252             return true;
    253         }
    254 
    255         return (/needsclick/).test(target.className);
    256     };
    257 
    258 
    259     /**
    260      * Determine whether a given element requires a call to focus to simulate click into element.
    261      *
    262      * @param {EventTarget|Element} target Target DOM element
    263      * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
    264      */
    265     FastClick.prototype.needsFocus = function(target) {
    266         switch (target.nodeName.toLowerCase()) {
    267         case 'textarea':
    268             return true;
    269         case 'select':
    270             return !deviceIsAndroid;
    271         case 'input':
    272             switch (target.type) {
    273             case 'button':
    274             case 'checkbox':
    275             case 'file':
    276             case 'image':
    277             case 'radio':
    278             case 'submit':
    279                 return false;
    280             }
    281 
    282             // No point in attempting to focus disabled inputs
    283             return !target.disabled && !target.readOnly;
    284         default:
    285             return (/needsfocus/).test(target.className);
    286         }
    287     };
    288 
    289 
    290     /**
    291      * Send a click event to the specified element.
    292      *
    293      * @param {EventTarget|Element} targetElement
    294      * @param {Event} event
    295      */
    296     FastClick.prototype.sendClick = function(targetElement, event) {
    297         var clickEvent, touch;
    298 
    299         // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
    300         if (document.activeElement && document.activeElement !== targetElement) {
    301             document.activeElement.blur();
    302         }
    303 
    304         touch = event.changedTouches[0];
    305 
    306         // Synthesise a click event, with an extra attribute so it can be tracked
    307         clickEvent = document.createEvent('MouseEvents');
    308         clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
    309         clickEvent.forwardedTouchEvent = true;
    310         targetElement.dispatchEvent(clickEvent);
    311     };
    312 
    313     FastClick.prototype.determineEventType = function(targetElement) {
    314 
    315         //Issue #159: Android Chrome Select Box does not open with a synthetic click event
    316         if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
    317             return 'mousedown';
    318         }
    319 
    320         return 'click';
    321     };
    322 
    323 
    324     /**
    325      * @param {EventTarget|Element} targetElement
    326      */
    327     FastClick.prototype.focus = function(targetElement) {
    328         var length;
    329 
    330         // 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.
    331         if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
    332             length = targetElement.value.length;
    333             targetElement.setSelectionRange(length, length);
    334         } else {
    335             targetElement.focus();
    336         }
    337     };
    338 
    339 
    340     /**
    341      * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
    342      *
    343      * @param {EventTarget|Element} targetElement
    344      */
    345     FastClick.prototype.updateScrollParent = function(targetElement) {
    346         var scrollParent, parentElement;
    347 
    348         scrollParent = targetElement.fastClickScrollParent;
    349 
    350         // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
    351         // target element was moved to another parent.
    352         if (!scrollParent || !scrollParent.contains(targetElement)) {
    353             parentElement = targetElement;
    354             do {
    355                 if (parentElement.scrollHeight > parentElement.offsetHeight) {
    356                     scrollParent = parentElement;
    357                     targetElement.fastClickScrollParent = parentElement;
    358                     break;
    359                 }
    360 
    361                 parentElement = parentElement.parentElement;
    362             } while (parentElement);
    363         }
    364 
    365         // Always update the scroll top tracker if possible.
    366         if (scrollParent) {
    367             scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
    368         }
    369     };
    370 
    371 
    372     /**
    373      * @param {EventTarget} targetElement
    374      * @returns {Element|EventTarget}
    375      */
    376     FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
    377 
    378         // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
    379         if (eventTarget.nodeType === Node.TEXT_NODE) {
    380             return eventTarget.parentNode;
    381         }
    382 
    383         return eventTarget;
    384     };
    385 
    386 
    387     /**
    388      * On touch start, record the position and scroll offset.
    389      *
    390      * @param {Event} event
    391      * @returns {boolean}
    392      */
    393     FastClick.prototype.onTouchStart = function(event) {
    394         var targetElement, touch, selection;
    395 
    396         // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
    397         if (event.targetTouches.length > 1) {
    398             return true;
    399         }
    400 
    401         targetElement = this.getTargetElementFromEventTarget(event.target);
    402         touch = event.targetTouches[0];
    403 
    404         if (deviceIsIOS) {
    405 
    406             // Only trusted events will deselect text on iOS (issue #49)
    407             selection = window.getSelection();
    408             if (selection.rangeCount && !selection.isCollapsed) {
    409                 return true;
    410             }
    411 
    412             if (!deviceIsIOS4) {
    413 
    414                 // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
    415                 // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
    416                 // with the same identifier as the touch event that previously triggered the click that triggered the alert.
    417                 // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
    418                 // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
    419                 // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
    420                 // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
    421                 // random integers, it's safe to to continue if the identifier is 0 here.
    422                 if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
    423                     event.preventDefault();
    424                     return false;
    425                 }
    426 
    427                 this.lastTouchIdentifier = touch.identifier;
    428 
    429                 // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
    430                 // 1) the user does a fling scroll on the scrollable layer
    431                 // 2) the user stops the fling scroll with another tap
    432                 // then the event.target of the last 'touchend' event will be the element that was under the user's finger
    433                 // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
    434                 // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
    435                 this.updateScrollParent(targetElement);
    436             }
    437         }
    438 
    439         this.trackingClick = true;
    440         this.trackingClickStart = event.timeStamp;
    441         this.targetElement = targetElement;
    442 
    443         this.touchStartX = touch.pageX;
    444         this.touchStartY = touch.pageY;
    445 
    446         // Prevent phantom clicks on fast double-tap (issue #36)
    447         if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
    448             event.preventDefault();
    449         }
    450 
    451         return true;
    452     };
    453 
    454 
    455     /**
    456      * Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
    457      *
    458      * @param {Event} event
    459      * @returns {boolean}
    460      */
    461     FastClick.prototype.touchHasMoved = function(event) {
    462         var touch = event.changedTouches[0], boundary = this.touchBoundary;
    463 
    464         if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
    465             return true;
    466         }
    467 
    468         return false;
    469     };
    470 
    471 
    472     /**
    473      * Update the last position.
    474      *
    475      * @param {Event} event
    476      * @returns {boolean}
    477      */
    478     FastClick.prototype.onTouchMove = function(event) {
    479         if (!this.trackingClick) {
    480             return true;
    481         }
    482 
    483         // If the touch has moved, cancel the click tracking
    484         if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
    485             this.trackingClick = false;
    486             this.targetElement = null;
    487         }
    488 
    489         return true;
    490     };
    491 
    492 
    493     /**
    494      * Attempt to find the labelled control for the given label element.
    495      *
    496      * @param {EventTarget|HTMLLabelElement} labelElement
    497      * @returns {Element|null}
    498      */
    499     FastClick.prototype.findControl = function(labelElement) {
    500 
    501         // Fast path for newer browsers supporting the HTML5 control attribute
    502         if (labelElement.control !== undefined) {
    503             return labelElement.control;
    504         }
    505 
    506         // All browsers under test that support touch events also support the HTML5 htmlFor attribute
    507         if (labelElement.htmlFor) {
    508             return document.getElementById(labelElement.htmlFor);
    509         }
    510 
    511         // If no for attribute exists, attempt to retrieve the first labellable descendant element
    512         // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
    513         return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
    514     };
    515 
    516 
    517     /**
    518      * On touch end, determine whether to send a click event at once.
    519      *
    520      * @param {Event} event
    521      * @returns {boolean}
    522      */
    523     FastClick.prototype.onTouchEnd = function(event) {
    524         var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
    525 
    526         if (!this.trackingClick) {
    527             return true;
    528         }
    529 
    530         // Prevent phantom clicks on fast double-tap (issue #36)
    531         if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
    532             this.cancelNextClick = true;
    533             return true;
    534         }
    535 
    536         if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
    537             return true;
    538         }
    539 
    540         // Reset to prevent wrong click cancel on input (issue #156).
    541         this.cancelNextClick = false;
    542 
    543         this.lastClickTime = event.timeStamp;
    544 
    545         trackingClickStart = this.trackingClickStart;
    546         this.trackingClick = false;
    547         this.trackingClickStart = 0;
    548 
    549         // On some iOS devices, the targetElement supplied with the event is invalid if the layer
    550         // is performing a transition or scroll, and has to be re-detected manually. Note that
    551         // for this to function correctly, it must be called *after* the event target is checked!
    552         // See issue #57; also filed as rdar://13048589 .
    553         if (deviceIsIOSWithBadTarget) {
    554             touch = event.changedTouches[0];
    555 
    556             // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
    557             targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
    558             targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
    559         }
    560 
    561         targetTagName = targetElement.tagName.toLowerCase();
    562         if (targetTagName === 'label') {
    563             forElement = this.findControl(targetElement);
    564             if (forElement) {
    565                 this.focus(targetElement);
    566                 if (deviceIsAndroid) {
    567                     return false;
    568                 }
    569 
    570                 targetElement = forElement;
    571             }
    572         } else if (this.needsFocus(targetElement)) {
    573 
    574             // 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.
    575             // 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).
    576             if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
    577                 this.targetElement = null;
    578                 return false;
    579             }
    580 
    581             this.focus(targetElement);
    582             this.sendClick(targetElement, event);
    583 
    584             // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
    585             // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
    586             if (!deviceIsIOS || targetTagName !== 'select') {
    587                 this.targetElement = null;
    588                 event.preventDefault();
    589             }
    590 
    591             return false;
    592         }
    593 
    594         if (deviceIsIOS && !deviceIsIOS4) {
    595 
    596             // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
    597             // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
    598             scrollParent = targetElement.fastClickScrollParent;
    599             if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
    600                 return true;
    601             }
    602         }
    603 
    604         // Prevent the actual click from going though - unless the target node is marked as requiring
    605         // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
    606         if (!this.needsClick(targetElement)) {
    607             event.preventDefault();
    608             this.sendClick(targetElement, event);
    609         }
    610 
    611         return false;
    612     };
    613 
    614 
    615     /**
    616      * On touch cancel, stop tracking the click.
    617      *
    618      * @returns {void}
    619      */
    620     FastClick.prototype.onTouchCancel = function() {
    621         this.trackingClick = false;
    622         this.targetElement = null;
    623     };
    624 
    625 
    626     /**
    627      * Determine mouse events which should be permitted.
    628      *
    629      * @param {Event} event
    630      * @returns {boolean}
    631      */
    632     FastClick.prototype.onMouse = function(event) {
    633 
    634         // If a target element was never set (because a touch event was never fired) allow the event
    635         if (!this.targetElement) {
    636             return true;
    637         }
    638 
    639         if (event.forwardedTouchEvent) {
    640             return true;
    641         }
    642 
    643         // Programmatically generated events targeting a specific element should be permitted
    644         if (!event.cancelable) {
    645             return true;
    646         }
    647 
    648         // Derive and check the target element to see whether the mouse event needs to be permitted;
    649         // unless explicitly enabled, prevent non-touch click events from triggering actions,
    650         // to prevent ghost/doubleclicks.
    651         if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
    652 
    653             // Prevent any user-added listeners declared on FastClick element from being fired.
    654             if (event.stopImmediatePropagation) {
    655                 event.stopImmediatePropagation();
    656             } else {
    657 
    658                 // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
    659                 event.propagationStopped = true;
    660             }
    661 
    662             // Cancel the event
    663             event.stopPropagation();
    664             event.preventDefault();
    665 
    666             return false;
    667         }
    668 
    669         // If the mouse event is permitted, return true for the action to go through.
    670         return true;
    671     };
    672 
    673 
    674     /**
    675      * On actual clicks, determine whether this is a touch-generated click, a click action occurring
    676      * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
    677      * an actual click which should be permitted.
    678      *
    679      * @param {Event} event
    680      * @returns {boolean}
    681      */
    682     FastClick.prototype.onClick = function(event) {
    683         var permitted;
    684 
    685         // 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.
    686         if (this.trackingClick) {
    687             this.targetElement = null;
    688             this.trackingClick = false;
    689             return true;
    690         }
    691 
    692         // 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.
    693         if (event.target.type === 'submit' && event.detail === 0) {
    694             return true;
    695         }
    696 
    697         permitted = this.onMouse(event);
    698 
    699         // 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.
    700         if (!permitted) {
    701             this.targetElement = null;
    702         }
    703 
    704         // If clicks are permitted, return true for the action to go through.
    705         return permitted;
    706     };
    707 
    708 
    709     /**
    710      * Remove all FastClick's event listeners.
    711      *
    712      * @returns {void}
    713      */
    714     FastClick.prototype.destroy = function() {
    715         var layer = this.layer;
    716 
    717         if (deviceIsAndroid) {
    718             layer.removeEventListener('mouseover', this.onMouse, true);
    719             layer.removeEventListener('mousedown', this.onMouse, true);
    720             layer.removeEventListener('mouseup', this.onMouse, true);
    721         }
    722 
    723         layer.removeEventListener('click', this.onClick, true);
    724         layer.removeEventListener('touchstart', this.onTouchStart, false);
    725         layer.removeEventListener('touchmove', this.onTouchMove, false);
    726         layer.removeEventListener('touchend', this.onTouchEnd, false);
    727         layer.removeEventListener('touchcancel', this.onTouchCancel, false);
    728     };
    729 
    730 
    731     /**
    732      * Check whether FastClick is needed.
    733      *
    734      * @param {Element} layer The layer to listen on
    735      */
    736     FastClick.notNeeded = function(layer) {
    737         var metaViewport;
    738         var chromeVersion;
    739         var blackberryVersion;
    740         var firefoxVersion;
    741 
    742         // Devices that don't support touch don't need FastClick
    743         if (typeof window.ontouchstart === 'undefined') {
    744             return true;
    745         }
    746 
    747         // Chrome version - zero for other browsers
    748         chromeVersion = +(/Chrome/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
    749 
    750         if (chromeVersion) {
    751 
    752             if (deviceIsAndroid) {
    753                 metaViewport = document.querySelector('meta[name=viewport]');
    754 
    755                 if (metaViewport) {
    756                     // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
    757                     if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
    758                         return true;
    759                     }
    760                     // Chrome 32 and above with width=device-width or less don't need FastClick
    761                     if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
    762                         return true;
    763                     }
    764                 }
    765 
    766             // Chrome desktop doesn't need FastClick (issue #15)
    767             } else {
    768                 return true;
    769             }
    770         }
    771 
    772         if (deviceIsBlackBerry10) {
    773             blackberryVersion = navigator.userAgent.match(/Version/([0-9]*).([0-9]*)/);
    774 
    775             // BlackBerry 10.3+ does not require Fastclick library.
    776             // https://github.com/ftlabs/fastclick/issues/251
    777             if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
    778                 metaViewport = document.querySelector('meta[name=viewport]');
    779 
    780                 if (metaViewport) {
    781                     // user-scalable=no eliminates click delay.
    782                     if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
    783                         return true;
    784                     }
    785                     // width=device-width (or less than device-width) eliminates click delay.
    786                     if (document.documentElement.scrollWidth <= window.outerWidth) {
    787                         return true;
    788                     }
    789                 }
    790             }
    791         }
    792 
    793         // IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97)
    794         if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
    795             return true;
    796         }
    797 
    798         // Firefox version - zero for other browsers
    799         firefoxVersion = +(/Firefox/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
    800 
    801         if (firefoxVersion >= 27) {
    802             // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896
    803 
    804             metaViewport = document.querySelector('meta[name=viewport]');
    805             if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {
    806                 return true;
    807             }
    808         }
    809 
    810         // IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version
    811         // http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx
    812         if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
    813             return true;
    814         }
    815 
    816         return false;
    817     };
    818 
    819 
    820     /**
    821      * Factory method for creating a FastClick object
    822      *
    823      * @param {Element} layer The layer to listen on
    824      * @param {Object} [options={}] The options to override the defaults
    825      */
    826     FastClick.attach = function(layer, options) {
    827         return new FastClick(layer, options);
    828     };
    829 
    830 
    831     if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
    832 
    833         // AMD. Register as an anonymous module.
    834         define(function() {
    835             return FastClick;
    836         });
    837     } else if (typeof module !== 'undefined' && module.exports) {
    838         module.exports = FastClick.attach;
    839         module.exports.FastClick = FastClick;
    840     } else {
    841         window.FastClick = FastClick;
    842     }
    843 }());
    View Code

    fastclick没有什么好注释的,源代码已经注释的比较完善了。需要重点关注的是FastClick.prototype.onTouchEnd 函数,这个是核心函数。

     1 FastClick.prototype.onTouchEnd = function(event) {
     2     //这里一堆定义 暂时不用关心
     3         var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
     4 
     5     //trackingClick会在touchstart中置为true,这里校验是否是一个完整的touch事件
     6         if (!this.trackingClick) {
     7             return true;
     8         }
     9 
    10     //点击过快 此次点击无效
    11         // Prevent phantom clicks on fast double-tap (issue #36)
    12         if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
    13             this.cancelNextClick = true;
    14             return true;
    15         }
    16 
    17     //touchend与touchstart间隔过长,则不再认为这是一个click事件
    18         if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
    19             return true;
    20         }
    21 
    22     //一些重置操作
    23         // Reset to prevent wrong click cancel on input (issue #156).
    24         this.cancelNextClick = false;
    25 
    26         this.lastClickTime = event.timeStamp;
    27 
    28         trackingClickStart = this.trackingClickStart;
    29         this.trackingClick = false;
    30         this.trackingClickStart = 0;
    31 
    32         // On some iOS devices, the targetElement supplied with the event is invalid if the layer
    33         // is performing a transition or scroll, and has to be re-detected manually. Note that
    34         // for this to function correctly, it must be called *after* the event target is checked!
    35         // See issue #57; also filed as rdar://13048589 .
    36         if (deviceIsIOSWithBadTarget) {
    37             touch = event.changedTouches[0];
    38 
    39             // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
    40             targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
    41             targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
    42         }
    43 
    44         targetTagName = targetElement.tagName.toLowerCase();
    45         if (targetTagName === 'label') {
    46             forElement = this.findControl(targetElement);
    47             if (forElement) {
    48                 this.focus(targetElement);
    49                 if (deviceIsAndroid) {
    50                     return false;
    51                 }
    52 
    53                 targetElement = forElement;
    54             }
    55         } else if (this.needsFocus(targetElement)) {//needsFocus:重点关注 发现这里才是我们代码不能好好工作的原因
    56                                                 //touchend取消默认事件后,靠focus给input text焦点
    57             // 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.
    58             // 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).
    59             if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
    60                 this.targetElement = null;
    61                 return false;
    62             }
    63 
    64             this.focus(targetElement);
    65             this.sendClick(targetElement, event);
    66 
    67       //这里若不是IOS 阻止默认事件,但我用IOS9测试,IOS9也需要阻止默认事件。
    68             // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
    69             // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
    70             if (!deviceIsIOS || targetTagName !== 'select') {
    71                 this.targetElement = null;
    72                 event.preventDefault();
    73             }
    74 
    75       //这个return 没看到用处
    76             return false;
    77         }
    78 
    79         if (deviceIsIOS && !deviceIsIOS4) {
    80 
    81             // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
    82             // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
    83             scrollParent = targetElement.fastClickScrollParent;
    84             if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
    85                 return true;
    86             }
    87         }
    88 
    89         // Prevent the actual click from going though - unless the target node is marked as requiring
    90         // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
    91         if (!this.needsClick(targetElement)) {
    92             event.preventDefault();
    93             this.sendClick(targetElement, event);
    94         }
    95 
    96         return false;
    97     };

    看到了fastclick的工作原理,修改我们的代码,最终如下:

     1 <!doctype html>
     2 <html>
     3 <head>
     4     <meta charset="utf-8">
     5     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
     6     <title>手机端点击</title>
     7 <style>
     8 body{margin:0;}
     9 input{width:90%;height:20px;}
    10 #demo1{padding-top:20px;}
    11 #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
    12 #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
    13 .hide{display:none;}
    14 </style>
    15 </head>
    16 <body>
    17 <div id="demo1">
    18     <input id="text">
    19 </div>
    20 <div id="demo2">
    21     <button id="btn">点击我</button>
    22 </div>
    23 <script>
    24     var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn"), text = document.querySelector("#text")
    25 
    26     document.addEventListener('click', function(e){
    27         if(e.ming){
    28             return true;
    29         }
    30         if (e.stopImmediatePropagation) {
    31             e.stopImmediatePropagation();
    32         } else {
    33             e.propagationStopped = true;
    34         }
    35         e.stopPropagation();
    36         e.preventDefault();
    37         return true;
    38     }, true)
    39     /*document.addEventListener('mousedown', function(e){
    40         if(e.ming){
    41             return true;
    42         }
    43         if (e.stopImmediatePropagation) {
    44             e.stopImmediatePropagation();
    45         } else {
    46             e.propagationStopped = true;
    47         }
    48         e.stopPropagation();
    49         e.preventDefault();
    50         return true;
    51     }, true)*/
    52 
    53     text.addEventListener("click", function(){
    54         console.log("text click")
    55     })
    56 
    57     text.addEventListener("touchend", function(){
    58         console.log("text touchend")
    59     })
    60 
    61     text.addEventListener("touchstart", function(){
    62         console.log("text touchstart")
    63     })
    64     text.addEventListener("mousedown", function(){
    65         console.log("text mousedown")
    66     })
    67 
    68     btn.addEventListener('click', function(e){
    69         console.log(e.ming);
    70         demo2.className = "hide";
    71     })
    72 
    73     var el
    74     document.addEventListener("touchstart", function(e){
    75         el = e.target
    76     })
    77     document.addEventListener("touchend", function(e){
    78         console.log('touchend')
    79         var event = document.createEvent("MouseEvents")
    80         event.initEvent("click", true, true)
    81         event.ming = true
    82         e.target.focus();
    83         el && el.dispatchEvent(event)
    84         e.preventDefault();
    85         return true;
    86     })
    87 </script>
    88 </body>
    89 </html>
    View Code

    正常工作,perfect

  • 相关阅读:
    如何解决"应用程序无法启动,因为应用程序的并行配置不正确"问题
    C/C++时间函数使用方法
    vim: C++文件之间快速切换(含视频)
    HOWTO install commonlisp on ubuntu
    TagSoup home page
    Quicklisp beta
    What is Lispbox?
    猫人女王
    Lisp in a box 安装指南 JAAN的专栏 博客频道 CSDN.NET
    Ubuntu 12.04 改造指南 | Ubuntusoft
  • 原文地址:https://www.cnblogs.com/ward/p/6096004.html
Copyright © 2011-2022 走看看