• Jump To … +
    anim.js core.js event.js io.js loader.js node.js touch.js
  • touch.js

  • ¶
    /*
    combined files : 
    
    m/touch
    
    */
  • ¶

    Touch 模块

    完成对常用移动设备触屏事件的封装,包括’tap’, ‘tapHold’, ‘singleTap’, ‘doubleTap’事件,用法和普通DOM事件一致

    • tap:轻触
    • tapHold:长按
    • singleTap:单击
    • doubleTap:双击
    var $ = S.all;
    $('#id').on('tap',function(e){
            alert('点击了tap事件');
    });
    
    /**
     * @ignore
     * @file touch
     * @author 虎牙 <huya.nzb@alibaba-inc.com>
     */
    !(function(S) {
        
        if (S.__touchModAdded) { return; }//预防重复绑定监听
        
        var TAP_MAX_TOUCH_TIME = 200, //KISSY为300ms
            TAP_MAX_DISTANCE = 10, //KISSY为5px
            TAP_HOLD_DELAY = 500, //KISSY为1000ms
            SINGLE_TAP_DELAY = 300, //KISSY为300ms,Zepto为250ms,太快无法触发doubleTap
            
            docElem = document.documentElement,
            hasTouch = !!('ontouchstart' in window),
  • ¶

    桌面浏览器上用mousedown和mouseup模拟

            EVT_TOUCH_START = hasTouch ? 'touchstart' : 'mousedown',
            EVT_TOUCH_MOVE = hasTouch ? 'touchmove' : 'mousemove',
            EVT_TOUCH_END = hasTouch ? 'touchend' : 'mouseup',
            EVT_TOUCH_CANCEL = 'touchcancel',
            EVT_SCROLL = 'scroll',
            
            noop = function() {},
            
            tapHoldTimer = null,
            doubleTapTimmer = null,
            singleTouch = null,
            
            touches = [
            /*{
                startX:0,
                startY:0,
                endX:0,
                endY:0,
                startTime:0,
                endTime:0,
                deltaX:0,
                deltaY:0,
                distance:0,
                timeSpan:0,
           }*/
           ];
        
        /* 清除多余触摸 */
        function clearTouchArray() {
            if (touches.length > 2) {
                var tmpArray = [];
                for (var i = 1; i < touches.length; i++) {
                    tmpArray[i - 1] = touches[i];
                }
                touches = tmpArray;
            }
        }
    
        /* 排除多次绑定中的单次点击的多次记录 */
        function shouldExcludeTouches() {
            
            clearTouchArray();
            
            if (touches.length == 0) {
                return false;
            }
            
            var duration = singleTouch.startTime - touches[touches.length - 1].startTime;
            
            /* 判断是否是同一次点击 */
            if (duration < 10) {
                return true;
            } else {
                return false;
            }
        }
        
        /* 检查是否是两次tap */
        function checkDoubleTap() {
            
            clearTouchArray();
            
            if (touches.length == 1) {
                return false;
            }
            
            /* 检查两次tap的target是不是一致 */
            var sameTarget = touches[0].endEvent.target == touches[1].endEvent.target;
            var duration = touches[1].startTime - touches[0].startTime;
            
            if (sameTarget && duration < SINGLE_TAP_DELAY) {
                return true;
            } else {
                return false;
            }
        }
        
        /* 取消长按的延时器 */
        function cancelTapHoldTimer() {
            if (tapHoldTimer) {
                clearTimeout(tapHoldTimer);
                tapHoldTimer = null;
            }
        }
        
        /* 取消双击的延时器 */
        function cancelDoubleTapTimer() {
            if (doubleTapTimmer) {
                clearTimeout(doubleTapTimmer);
                doubleTapTimmer = null;
            }
        }
        
        /* 触摸开始回调 */
        function touchstartHandler(e) {
            
            /* 多指触摸 */
            if (e.touches && e.touches.length > 1) {
                singleTouch = null;
                cancelDoubleTapTimer();
                return;
            }
            
            var target = e.target,
                touch = e.changedTouches ? e.changedTouches[0] : e,
                startX = touch.pageX,
                startY = touch.pageY;
                
            singleTouch = {
                startX: startX,
                startY: startY,
                startEvent: e,
                startTime: new Date().getTime()
            };
            
            /* 长按延时器 */
            cancelTapHoldTimer();
            
            /* 设置长按延时 */
            tapHoldTimer = setTimeout(function() {
                
                cancelTapHoldTimer();
                
                if (singleTouch) {
                    var eProxy = S.merge(e, {
                        type: 'tapHold',
                        pageX: startX,
                        pageY: startY,
                        originalEvent: e,
                        timeStamp: new Date().getTime()
                    });
                    
                    S.one(target).fire('tapHold', eProxy);
                }
                
            }, TAP_HOLD_DELAY);
        }
        
        /* 触摸滑动回调 */
        function touchmoveHandler(e) {
            
            if (!singleTouch || !tapHoldTimer) { return; }
            
            var target = e.target,
                touch = e.changedTouches ? e.changedTouches[0] : e,
                endX = touch.pageX,
                endY = touch.pageY,
                deltaX = Math.abs(endX - singleTouch.startX), //滑过的水平距离
                deltaY = Math.abs(endY - singleTouch.startY), //滑过的垂直距离
                distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
            
            /* 位移超过一定距离,取消tapHold事件 */
            if (distance > TAP_MAX_DISTANCE) {
                cancelTapHoldTimer();
            }
        }
        
        /* 触摸结束回调 */
        function touchendHandler(e) {
            
            cancelTapHoldTimer();
            
            if (!singleTouch) {
                cancelDoubleTapTimer();
                return;
            }
            
            var eProxy, $target,
                target = e.target,
                touch = e.changedTouches ? e.changedTouches[0] : e,
                endX = touch.pageX,
                endY = touch.pageY,
                endTime = new Date().getTime(),
                deltaX = Math.abs(endX - singleTouch.startX), //滑过的水平距离
                deltaY = Math.abs(endY - singleTouch.startY); //滑过的垂直距离
                
            S.mix(singleTouch, {
                endX: endX,
                endY: endY,
                deltaX: deltaX,
                deltaY: deltaY,
                endTime: endTime,
                endEvent: e,
                distance: Math.sqrt(deltaX * deltaX + deltaY * deltaY),
                timeSpan: endTime - singleTouch.startTime
            });
            
            /* 触摸时间和移动距离超过最大值,则无效 */
            if (singleTouch.timeSpan > TAP_MAX_TOUCH_TIME || singleTouch.distance > TAP_MAX_DISTANCE) {
                singleTouch = null;
                cancelDoubleTapTimer();
                return;
            }
            
            /* 同时绑定singleTap和doubleTap时,一次点击push了两次singleTouch,应该只push一次 */
            if (!shouldExcludeTouches(singleTouch)) {
                touches.push(singleTouch);
            } else {
                cancelDoubleTapTimer();
                return;
            }
            
            clearTouchArray();
           
            fireTap(e, {
                type: 'tap',
                pageX: endX,
                pageY: endY,
                clientX: touch.clientX,
                clientY: touch.clientY,
                timeStamp: new Date().getTime(),
                originalEvent:e
            });
        }
        
        /* 触摸取消回调 */
        function touchcancelHandler(e) {
            singleTouch = null;
            clearTouchArray();
            cancelDoubleTapTimer();
            cancelTapHoldTimer();
        }
        
        /* TODO
        部分情况不可以发生tap事件,或者target有误,比如说-webkit-overflow-scrolling等
        触发tap事件*/
        function fireTap(e, obj) {
            var  $target = S.one(e.target);
            
            eProxy = S.merge(e, obj || {});
            
            /* 防止点击穿透 */
            eProxy.preventDefault = function() {
                try { 
                    this.constructor.prototype.preventDefault.call(this);   
                    e.preventDefault(); 
                } catch(err) {}
            };
            
            /* 先触发tap,再触发doubleTap */
            $target.fire('tap', eProxy);
            
            /* doubleTap和singleTap互斥 */
            if (doubleTapTimmer) {
                cancelDoubleTapTimer();
                
                if (checkDoubleTap()) {
                    S.mix(eProxy, {
                        type: 'doubleTap',
                        timeStamp: new Date().getTime()
                    });
                    
                    $target.fire('doubleTap', eProxy);
                }
            } else {
                doubleTapTimmer = setTimeout(function() {
                    clearTimeout(doubleTapTimmer);
                    doubleTapTimmer = null;
                    
                    S.mix(eProxy, {
                        type: 'singleTap',
                        timeStamp: new Date().getTime()
                    });
                    
                    $target.fire('singleTap', eProxy);
                }, SINGLE_TAP_DELAY);
            }
        }
        
        docElem.addEventListener(EVT_TOUCH_START, touchstartHandler, false);
        docElem.addEventListener(EVT_TOUCH_MOVE, touchmoveHandler, false);
        docElem.addEventListener(EVT_TOUCH_END, touchendHandler, false);
        docElem.addEventListener(EVT_TOUCH_CANCEL, touchcancelHandler, false);
        window.addEventListener(EVT_SCROLL, touchcancelHandler, false);
        
        /* fix ios 的 touch 无法触发的问题 */
        if (hasTouch) {
            
            S.each(['tap', 'tapHold', 'singleTap', 'doubleTap'], function(item) {
                S.Event.Special[item] = {
                    setup: function() {
                        var elem = this[0];
                        if (!elem.__fixTouchEvent) {
                            elem.addEventListener('touchstart', noop, false);
                            elem.__fixTouchEvent = true;
                        }
                    }
                };
            });
            
            S.ready(function() {
                document.body.addEventListener('touchstart', noop, false);
            });
        }
    
        S.add && S.add('touch', function () {});
        
        S.__touchModAdded = true;
    
    })(KISSY);
    
    /**
     * 解决点击穿透的问题
     * [http://gitlab.alibaba-inc.com/mpi/fix-click-through](http://gitlab.alibaba-inc.com/mpi/fix-click-through)
     * @author huya.nzb@alibaba-inc.com
     * @date 2015-01-09
     */
  • ¶

    解决tap点击穿透的问题

    为了解决 click 事件的300ms的延时,大部分js库实现了tap事件,原理是利用touch事件进行封装,然后适当的阻止默认的click事件,但通常情况下会发生tap穿透。重要参考资料:

    • 移动端点击事件的分析 @皓勋
    • fix-click-through (解决点击穿透的js库)@虎牙

    fix-click-through 代码不依赖任何库,直接使用

    //tap事件(已集成进KISSY mini touch模块中)
    FixClickThrough('tap');
    
    //iScroll的模拟click
    FixClickThrough('click', function(e) {
    
        //iScroll4/5
        return e._fake || e._constructed;
    });
    

    单独引用 fix-click-through

    <script src="
        http://g.alicdn.com/mpi/fix-click-through/0.0.4/index.js">
    </script>
    

    目前该解决方案可以阻止穿透后的元素(包括表单元素 input、select 等)的 click、mousedown、mouseup 事件,但是无法阻止 :active 和 -webkit-tap-highlight-color 的状态触发,暂时没有很好的解决方案

    !(function() {
        
        /* TODO
        解决点击穿透之后active和-webkit-tap-highlight-color无法取消的问题 */
        
        var docElem = document.documentElement,
        
            CLICK = 'click',
            MOUSE_DOWN = 'mousedown',
            MOUSE_UP = 'mouseup',
            RADIO_TYPE = 'radio',
            RESET_DELAY = 400,
            THRESHOLD = 10,
            
            ATTR_FIX_THROUGH = 'fix-through',
            ATTR_FIX_THROUGH_TAPPED = 'fix-through-tapped';
        
        (function () {
            
            if (document.getElementById('fix-click-through-style')) { return; }
            
            var stylesheet = document.createElement('style'),
                head = document.getElementsByTagName('head')[0];
                
            stylesheet.type = 'text/css';
            stylesheet.id = 'fix-click-through-style';
            head.insertBefore(stylesheet, head.firstChild);
            
            /*默认样式为黑色*/
            stylesheet.appendChild(document.createTextNode(
                '[fix-through] input,' +
                '[fix-through] select,' +
                '[fix-through] textarea {pointer-events:none;}' +
                
                '[fix-through] input[type=button],' +
                '[fix-through] input[type=submit],' +
                '[fix-through] input[type=reset],' +
                '[fix-through] input[type=image],' +
                '[fix-through] input[type=file],' +
                '[fix-through] input[type=radio],' +
                '[fix-through] input[type=checkbox],' +
                '[fix-through] [fix-through-tapped] {pointer-events:auto;}'
            ));
        })();
        
        /**
         * 解决点击穿透
         * @class FixClickThrough
         * @static
         */    
        window.FixClickThrough = window.FixClickThrough || {
            
            /**
             * 事件绑定缓存
             * @property cache
             * @type Object
             */
            cache: {},
            
            /**
             * elementFromPoint是否相对于视窗
             * @property relativeToViewport
             * @type Boolean
             */
            relativeToViewport: null,
            
            /**
             * 判断elementFromPoint是否相对于视窗
             * @method isRelativeToViewport
             * @return {Boolean}
             */
            isRelativeToViewport: function() {
                if (this.relativeToViewport !== null) return this.relativeToViewport;
            
                var x = window.pageXOffset ? window.pageXOffset + window.innerWidth - 1 : 0;
                var y = window.pageYOffset ? window.pageYOffset + window.innerHeight - 1 : 0;
                if (!x && !y) return true;
              
    			/* 通过检测一个比viewport更大的点来测试,如果返回一个元素
    			 * 则说明elementFromPoint包含一个页面坐标*/
                return (this.relativeToViewport = !!document.elementFromPoint && !document.elementFromPoint(x, y));
            },
            
            /**
             * 根据坐标获取元素
             * [https://github.com/moll/js-element-from-point/](https://github.com/moll/js-element-from-point/)
             * @method elementFromPoint
             * @param {Number} x X轴值
             * @param {Number} y Y轴值
             * @return {HTMLElement}
             */
            elementFromPoint: function(x, y) {
                if (!this.isRelativeToViewport())  {
                    x += window.pageXOffset;
                    y += window.pageYOffset;
                }
                return document.elementFromPoint ? document.elementFromPoint(x, y) : null;
            },
            
            /**
             * 是否源自于label节点
             * @method fromLabel
             * @param {HTMLElement} elem 事件新节点
             * @param {HTMLElement} from 事件源自于哪个节点
             * @return {Boolean} 是否源自于label节点
             */
            fromLabel: function(elem, from) {
                if (!elem || !elem.nodeName.match(/(input|select|textarea)/i)) {
                    return false;
                }
                
                var labels = this.getLabels(elem);
                
                for (var i = 0, l = labels.length; i < l; i++) {
                    if (labels[i].contains(from)) {
                        return true;
                    }
                }
                
                return false;
            },
            
            /**
             * 获取表单元素的label
             * @method getLabels
             * @param {HTMLElement} elem 表单元素
             * @return {Array} label数组
             */
            getLabels: function(elem) {
                var id = elem.id,
                    labels = [],
                    label = id ? document.querySelectorAll('label[for="' + id +'"]') : null;
                
                if (label && label.length) {
                    labels = labels.concat(labels.slice.call(label, 0));
                }
                
                label = elem;
                
                while ((label = label.parentNode)) {
                    if (label.nodeName.match(/label/i)) {
                        labels.push(label);
                    }
                } 
                
                return labels;  
            },
            
            /**
             * 绑定点击穿透的事件
             * @method bind
             * @param {String} eventName 事件名称
             * @param {Function} filter 过滤事件
             */
            bind: function(eventName, filter) {
                
                var self = this;
                
                /* 如果已经解决过,则不再解决 */
                if (!filter && this.cache[eventName]) { return; }
                
                /* 监听冒泡事件 */
                document.addEventListener(eventName, function(e) {
                    
                    var target = e.target,
                        now = new Date().getTime(),
                        halt, detach, detached, checked, newTarget;
                    
                    if (filter && filter(e) === false) { return; }
                    
                    /* 防止无法收起键盘 */
                    if (document.activeElement && 
                        document.activeElement.blur &&
                        document.activeElement !== target) {
                        
                        document.activeElement.blur();
                    }
                    
    				/*
                     * //防止点击输入框无法focus问题
                     *if (target.focus && document.activeElement !== target) {
                     * // target.focus();
                     *}
                     *
                     *如果当前位置的元素不是之前的元素,说明tap时发生了位移或者隐藏,直接阻止事件
                     *但是还存在tap事件后延时发生位移和隐藏的元素,这个时候最好触发一下
    				 */
                    newTarget = this.elementFromPoint(e.clientX, e.clientY);
                    
                    if (newTarget && newTarget !== target) {
                        e.preventDefault && e.preventDefault();
                    }
                    
                    /* 阻止事件穿透(click, focus, blur, focusin, focusout...) */
                    halt = function(evt) {
                        
                        newTarget = evt.target;
                        
                        if (newTarget !== target && !self.fromLabel(newTarget, target) &&
                            Math.abs(e.clientX - evt.clientX) < THRESHOLD &&
                            Math.abs(e.clientY - evt.clientY) < THRESHOLD) {
                            
                            /*大部分情况下可以阻止穿透点击事件的触发
                            某些浏览器和webview阻止了focus的触发,但浏览器依旧响应状态弹出键盘*/
                            e.preventDefault && e.preventDefault();
                            evt.preventDefault && evt.preventDefault();
                            evt.stopPropagation && evt.stopPropagation();
                            
                            /*部分安卓2.x手机(小米1)不支持stopImmediatePropagation*/
                            if (evt.stopImmediatePropagation) {
                                evt.stopImmediatePropagation();
                            }
                            
                            /*点击穿透到radio时,无法阻止选中的状态*/
                            if (newTarget.type === RADIO_TYPE) {
                                if (evt.type === MOUSE_DOWN) {
                                    checked = newTarget.checked;
                                } else if (checked === false && evt.type === CLICK) {
                                    newTarget.checked = false;
                                }
                            }
                            
                            /*最后触发穿透后解除绑定*/
                            if (evt.type === CLICK) {
                                detach();
                            }
                        }
                    };
                    
                    detach = function() {
                        if (detached) { return; }
                        
                        document.removeEventListener(CLICK, halt, true);
                        document.removeEventListener(MOUSE_DOWN, halt, true);
                        document.removeEventListener(MOUSE_UP, halt, true);
                        
                        /*如果值和之前设置的不一样,那么说明有可能连续触发了两次tap,
                        等待最后一次延时结束后移除attribute*/
                        if (target.getAttribute(ATTR_FIX_THROUGH_TAPPED) == now) {
                            target.removeAttribute(ATTR_FIX_THROUGH_TAPPED);
                        }
                        
                        if (docElem.getAttribute(ATTR_FIX_THROUGH) == now) {
                            docElem.removeAttribute(ATTR_FIX_THROUGH);
                        }
                        
                        detached = true;
                    };
                    
                    document.addEventListener(CLICK, halt, true);
                    document.addEventListener(MOUSE_DOWN, halt, true);
                    document.addEventListener(MOUSE_UP, halt, true);
                    
                    target.setAttribute(ATTR_FIX_THROUGH_TAPPED, now);
                    docElem.setAttribute(ATTR_FIX_THROUGH, now);
                    
                    /*在部分机型下,包括ios,click事件有可能延时300+ms,400ms是比较稳妥的时间*/
                    setTimeout(detach, RESET_DELAY);
                    
                }, false);
                
                /*没有过滤器的时候缓存*/
                if (!filter) {
                    this.cache[eventName] = 1;
                }
            }
        };
        
        /*解决tap事件穿透问题*/
        FixClickThrough.bind('tap');
        
    })();
  • ¶