如何绑定'touchstart'和'click'事件但不对两者同时响应?

217
我正在开发一个移动网站,它需要在各种设备上运行。目前让我头疼的是黑莓设备。
我们需要支持键盘点击和触摸事件。
理想情况下,我只需使用:
$thing.click(function(){...})

但我们遇到的问题是,一些黑莓设备在触摸屏幕后触发点击的时间会有非常烦人的延迟。

解决方法是改用touchstart:

$thing.bind('touchstart', function(event){...})
但是我该如何绑定两个事件,但只触发其中一个?我仍然需要 click 事件来支持键盘设备,但当然,如果我使用的是触摸设备,则不希望 click 事件被触发。

附加问题:是否有办法同时适应甚至没有 touchstart 事件的浏览器?研究后发现,BlackBerry OS5 不支持 touchstart 事件,因此也需要依赖 click 事件。

补充:

也许更全面的问题是:

使用 jQuery,是否可能/建议使用相同的绑定处理触摸交互和鼠标交互?

理想情况下,答案是肯定的。如果不行,我有一些选择:

  1. 我们可以使用 WURFL 来获取设备信息,因此可以创建自己的设备矩阵。根据设备,我们将使用 touchstart 或 click。
  2. 通过 JS 检测浏览器是否支持触摸(我需要进行更多的研究,但似乎是可行的)。

但这仍然存在一个问题:对于同时支持触摸屏和键盘的设备怎么办呢?我们支持的一些手机(即诺基亚和黑莓)都具有触摸屏和键盘。因此,这让我又回到了最初的问题...是否有办法同时允许两者?


2
最好绑定touchstart和touchend事件,并在您的触摸逻辑旁编写自己的单击逻辑。内置的click回调函数不知道触摸操作。 - Justin808
@DA - 不,你根本不需要绑定到 .click() 回调函数。我会尝试用一些伪代码来写出答案。我手头没有触摸设备来编写真正的代码 :) - Justin808
啊,但是需要澄清的是,我仍然需要点击事件,因为会有使用非触摸设备访问此网站的人。 - DA.
2
使用.bind('touchstart mouseup')可以解决这个问题(基于下面的评论之一) - oriadam
请查看.one方法:http://api.jquery.com/one。 - oriadam
显示剩余4条评论
37个回答

0

我在这里给出了一个答案,并用jsfiddle进行了演示。您可以检查不同的设备并报告它。

基本上,我使用一种事件锁定和一些相关函数:

/*
 * Event lock functions
 * ====================
 */
function getEventLock(evt, key){
   if(typeof(eventLock[key]) == 'undefined'){
      eventLock[key] = {};
      eventLock[key].primary = evt.type;
      return true;
   }
   if(evt.type == eventLock[key].primary)
      return true;
   else
      return false;
}

function primaryEventLock(evt, key){
   eventLock[key].primary = evt.type;
}

然后,在我的事件处理程序中,我首先发出对我的锁的请求:

/*
 * Event handlers
 * ==============
 */
$("#add").on("touchstart mousedown", addStart);
$("#add").on("touchend mouseup", addEnd);
function addStart(evt){
   // race condition between 'mousedown' and 'touchstart'
   if(!getEventLock(evt, 'add'))
      return;

   // some logic
   now = new Date().getTime();
   press = -defaults.pressDelay;
   task();

   // enable event lock and(?) event repetition
   pids.add = setTimeout(closure, defaults.pressDelay);

   function closure(){
        // some logic(?): comment out to disable repetition
      task();

      // set primary input device
      primaryEventLock(evt, 'add');

      // enable event repetition
      pids.add = setTimeout(closure, defaults.pressDelay);
   }
}
function addEnd(evt){
      clearTimeout(pids.add);
}

我必须强调的是,问题不在于简单地响应一个事件,而是不要在两个事件上都响应。

最后,在jsfiddle上有一个链接,其中介绍了一个更新版本,通过在事件开始和结束处理程序中添加对我的事件锁定库的简单调用以及2个作用域变量eventLockeventLockDelay,可以对现有代码产生最小的影响。


0

这里有一个简单的方法:

// A very simple fast click implementation
$thing.on('click touchstart', function(e) {
  if (!$(document).data('trigger')) $(document).data('trigger', e.type);
  if (e.type===$(document).data('trigger')) {
    // Do your stuff here
  }
});

基本上,您需要将触发的第一个事件类型保存到附加到根文档的jQuery数据对象中的“trigger”属性中,并且仅在事件类型等于“trigger”中的值时执行。在触摸设备上,事件链可能是“touchstart”后跟“click”; 但是,“click”处理程序不会被执行,因为“click”与“trigger”中保存的初始事件类型(“touchstart”)不匹配。

假设,我相信这是一个安全的假设,即您的智能手机不会自动从触摸设备变成鼠标设备,否则轻敲将永远不会注册,因为“trigger”事件类型仅在每个页面加载时保存一次,“click”永远不会匹配“touchstart”。

这里有一个CodePen供您玩耍(在触摸设备上尝试点击按钮-不应该有单击延迟):http://codepen.io/thdoan/pen/xVVrOZ

我还将其实现为一个简单的jQuery插件,通过传递选择器字符串也支持jQuery的后代过滤:

// A very simple fast click plugin
// Syntax: .fastClick([selector,] handler)
$.fn.fastClick = function(arg1, arg2) {
  var selector, handler;
  switch (typeof arg1) {
    case 'function':
      selector = null;
      handler = arg1;
      break;
    case 'string':
      selector = arg1;
      if (typeof arg2==='function') handler = arg2;
      else return;
      break;
    default:
      return;
  }
  this.on('click touchstart', selector, function(e) {
    if (!$(document).data('trigger')) $(document).data('trigger', e.type);
    if (e.type===$(document).data('trigger')) handler.apply(this, arguments);
  });
};

Codepen: http://codepen.io/thdoan/pen/GZrBdo/

代码笔:http://codepen.io/thdoan/pen/GZrBdo/

你为什么在文档上使用数据属性而不是只使用变量呢? - Raphael Schweikert
@RaphaelSchweikert 可能只是出于习惯。没有对错之分,但我喜欢使用 $.data() 存储可以被多个函数访问但不属于全局范围的数据。一个好处是,由于我将其存储在元素中,它自然地为我提供了更多的上下文。 - thdoan

0
如果你已经在使用jQuery,有两种处理此问题的简短而又有效的方法。我认为这并不需要像大多数答案那样复杂...
  1. 返回false;
只需监听任何/所有事件,并在结尾处添加"return false;"以停止额外的重复事件处理即可。
thing$.on('click touchend',function(){
    // do Stuff
    return false; // stop
});

示例可重复使用:

function bindClickAndTouchEvent(__targets$, __eventHandler){
    
    __targets$.on('click touchend',function(){
        __eventHandler.apply(this,arguments);// send original event
        return false;// but stop additional events
    });
}

// In-Line Usage:

bindClickAndTouchEvents( $('#some-element-id'), function(){ 
    console.log('Hey look only one click even when using touch on a touchscreen laptop') 
});

2. 鼠标抬起和触摸结束
根据你的使用情况,你可能只需要使用 mouseup 和 touchend,因为这两个事件根本不重叠,一开始只创建一个事件...那么你甚至不需要 "return false;"。
thing$.on('mouseup touchend',function(){
    // do Stuff
});

0

利用点击事件总是会跟随触摸事件的事实,我做了以下操作来消除“幽灵点击”,而不必使用超时或全局标志。

$('#buttonId').on('touchstart click', function(event){
    if ($(this).data("already")) {
        $(this).data("already", false);
        return false;
    } else if (event.type == "touchstart") {
        $(this).data("already", true);
    }
    //your code here
});

基本上每当元素上触发ontouchstart事件时,一个标志被设置,然后在单击发生时随后被移除(并忽略)。


0
如果您正在使用jQuery,以下内容对我来说非常有效:
var callback; // Initialize this to the function which needs to be called

$(target).on("click touchstart", selector, (function (func){
    var timer = 0;
    return function(e){
        if ($.now() - timer < 500) return false;
        timer = $.now();
        func(e);
    }
})(callback));

其他解决方案也很好,但我在循环中绑定了多个事件,并需要自调用函数创建适当的闭包。此外,我不想禁用绑定,因为我希望它可以在下一次点击/触摸开始时被激活。

可能会对类似情况的某人有所帮助!


0

我正在尝试这个,目前看起来可行(但我只在Android/Phonegap上测试过,买家需自负风险)

  function filterEvent( ob, ev ) {
      if (ev.type == "touchstart") {
          ob.off('click').on('click', function(e){ e.preventDefault(); });
      }
  }
  $('#keypad').on('touchstart click', '.number, .dot', function(event) {
      filterEvent( $('#keypad'), event );
      console.log( event.type );  // debugging only
           ... finish handling touch events...
  }

我不喜欢每次触摸都要重新绑定处理程序的事实,但总体来说,在计算机时间内触摸并不经常发生!

我有很多像“#keypad”一样的处理程序,因此拥有一个简单的函数让我能够处理问题而不需要太多代码,这就是我选择这种方式的原因。


0
为什么不使用jQuery事件API?

http://learn.jquery.com/events/event-extensions/

我已经成功地使用了这个简单的事件。它干净、可命名空间化,并且足够灵活,可以进一步改进。

var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
var eventType = isMobile ? "touchstart" : "click";

jQuery.event.special.touchclick = {
  bindType: eventType,
  delegateType: eventType
};

1
建议使用特性检测而非用户代理嗅探。 - jinglesthula
问题在于支持触摸和点击的设备。 - DA.

0

对于简单的功能,只需识别触摸或点击,我使用以下代码:

var element = $("#element");

element.click(function(e)
{
  if(e.target.ontouchstart !== undefined)
  {
    console.log( "touch" );
    return;
  }
  console.log( "no touch" );
});

如果定义了touchstart事件,它将返回“touch”,如果没有定义,则返回“no touch”。就像我说的,这只是一个简单的方法来处理点击/轻触事件。

0

更新:

我一直在研究如何实现同时使用点击和触摸结束事件来执行同一个函数,而且如果类型发生变化,该函数会有效地阻止事件。我的目标是拥有更具响应性的应用程序界面 - 我想减少事件开始到 UI 反馈循环的时间。

为了使这个实现工作,假设您已经将所有相关事件添加到“click”和“touchend”上。这可以防止一个元素被剥夺事件冒泡,如果两个事件都需要运行,但是它们的类型不同。

这里是一个基于轻量级 API 的实现,我已经简化了演示目的。它演示了如何在折叠元素上使用功能。

var tv = {
    /**
     * @method eventValidator()
     * @desc responsible for validating event of the same type.
     * @param {Object} e - event object
     * @param {Object} element - element event cache
     * @param {Function} callback - callback to invoke for events of the same type origin
     * @param {Object} [context] - context to pass to callback function
     * @param {Array} [args] - arguments array to pass in with context. Requires context to be passed
     * @return {Object} - new event cache
     */
    eventValidator: function(e, element, callback, context, args){
        if(element && element.type && element.type !== e.type){
            e.stopPropagation();
            e.preventDefault();
            return tv.createEventCacheObj({}, true);
        } else {
            element = tv.createEventCacheObj(e);
            (typeof context === "object" ? callback.apply(context, args) : callback());
            return element;
        }
    },

    /**
     * @method createEventCacheObj()
     * @param {Object} event - event object
     * @param {String} [event.type] - event type
     * @param {Number} [event.timeStamp] - time of event in MS since load
     * @param {Boolean} [reset=false] - flag to reset the object
     * @returns {{type: *, time: string}}
     */
    createEventCacheObj: function (event, reset){
        if(typeof reset !== 'boolean') reset = false;
        return {
            type: !reset ? event.type : null,
            time: !reset ? (event.timeStamp).toFixed(2): null
        };
    }
};

// Here is where the magic happens
var eventCache = [];
var pos = 0;

var $collapses = document.getElementsByClassName('tv-collapse__heading');
    Array.prototype.forEach.call($collapses, function(ele){
        ele.addEventListener('click', toggleCollapse);
        ele.addEventListener('touchend', toggleCollapse);

        // Cache mechanism
        ele.setAttribute('data-event-cache', String(pos++));
    });

/**
 * @func toggleCollapse()
 * @param {Object} e - event object
 * @desc responsible for toggling the state of a collapse element
 */
function toggleCollapse(e){
    eventCache[pos] = tv.eventValidator(e, eventCache[pos], function(){
       // Any event which isn't blocked will run the callback and its content
       // the context and arguments of the anonymous function match the event function context and arguments (assuming they are passed using the last two parameters of tv.eventValidator)

    }, this, arguments);
}

这是一个修改自Rafael Fragoso答案的响应 - 纯JS。

(function(){
  
  button = document.getElementById('sayHi');
  
  button.addEventListener('touchstart', ohHai);
  button.addEventListener('click', ohHai);

  function ohHai(event){
    event.stopPropagation();
    event.preventDefault();
    console.log('ohHai is:', event.type);
  };
})();
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SO - Answer</title>
</head>
<body>
  <button id="sayHi">Anyone there?</button>
</body>
</html>

在以下内容上运行片段并注意输出:

  • 手机
  • 平板电脑
  • 平板电脑(桌面模式-如果适用)
  • 台式电脑
  • 台式电脑(触摸屏-如果适用)

关键是我们阻止连续事件的触发。移动浏览器尽力模拟点击,当触摸发生时。我希望我能找到一篇文章的链接,它解释了所有在touchstart到click之间发生的事件。(实际上我正在寻找双击和单击之间300ms延迟的原因)。

触摸和鼠标设备

我使用Surface Pro和Windows 10桌面电脑进行了几次测试。我发现它们都会触发事件,就像你预期的那样,对于触摸是touchstart,对于触控板、鼠标和笔是click。有趣的是,一个接近但不在按钮上的触摸事件会触发一个没有触摸事件的点击事件。似乎Windows 10中内置的功能会在半径范围内查找最近的节点,如果找到一个节点,它将触发基于鼠标的事件。

相同类型的多个事件

如果一个元素上有两个相同类型的事件,阻止事件冒泡可能会防止其中一个事件触发。有几种不同的方法可以使用某种缓存来处理这个问题。我的初步想法是修改事件对象,但我们只获得了一个引用,所以我认为缓存解决方案将不得不足够。

0

将事件分配为'touchstart mousedown''touchend mouseup'可能是有效的,以避免使用click产生不良副作用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接