スマートフォンブラウザのjQuery Clickイベントに関すること


最近、はてなブックマークで「スマートフォンブラウザ不具合特集」というのを見つけました。知らなかったのもいくつかあったのですごく参考になりましたよ。
特にAndroid系は私も認識していないことが多いので、その辺りをきちんと見てる人はすごいなぁと思います。
3年前ぐらいに比べると、あっという間にAndroidが多機種化してきて、あっという間にガラケーチェック時代っぽくなりましたね。ため息が出ます。。。


去年の秋ごろに私が書いたエントリーの「iOS5のMobile Safariから使えるようになったHTML5・CSSを試してみました【前編】」でも、iPhoneで上手くいかない事例について少し触れていました。
iPhoneだとLabel要素をタップしてInput要素(例えばチェックボックスなど)を操作できないというものです。
この解決方法はlabel要素に空のonclick属性をつけることで解決できます。
簡単なチェックページを作ってみました。 > Input & Label Test


少しマニアックな事例ではありますが、最近jQueryをつかってスマートフォンブラウザ向けのサイトを作っていて気づいたことがあります。
jQueryにはイベントをバインドするメソッドがいくつかありますが、そのメソッドによってはタップに反応する領域が変わるというものです。

jQueryのイベントバインドにおける問題

jQueryでclickイベントをバインドしたいと思った場合、以下の6つのメソッドが使えると思います。

これら6つで同じようにイベントをバインドするページを作成しましたので、そちらをiPhone, Androidなどで見てください。
Click Event Bindのテストページ

テスト用のページには以下のようなコードが書かれています。

<!-- Javascript -->
$(function(){
  $('#click a').click(function(){
    alert('#click a');
  });
  $('#bind a').bind('click', function(){
    alert('#bind a');
  });
  $('#live a').live('click', function(){
    alert('#live a');
  });
  $('#delegate').delegate('a', 'click', function(){
    alert('#delegate a');
  });
  $('#on').on('click', 'a', function(){
    alert('#on a');
  });
  $('#one').on('click', 'a', function(){
    alert('#one a');
  });
});

<!-- HTML -->
<div id="click">
  This element is binding click event by '.click()' method.
  <a href="#click">.click()</a>
</div>
<div id="bind">
  This element is binding click event by '.bind()' method.
  <a href="#bind">.bind()</a>
</div>
<div id="live">
  This element is binding click event by '.live()' method.
  <a href="#live">.live()</a>
</div>
<div id="delegate">
  This element is binding click event by '.delegate()' method.
  <a href="#delegate">.delegate()</a>
</div>
<div id="on">
  This element is binding click event by '.on()' method.
  <a href="#on">.on()</a>
</div>
<div id="one">
  This element is binding click event by '.one()' method.
  <a href="#one">.one</a>
</div>

これで比較すると、delegate, on, oneでバインドしたElementは、本来意図していない要素にもタップイベント(iPhoneの場合、タップすると半透明のグレイがかかる)が発生するようになります。
テスト用のページの場合、ボタン(a要素)の下の何もない青色地のスペースをタップしてみるとわかりやすいです。

jQueryを詳しく読んでみることにします。
最新のjQuery(v.1.7.2)でこれら該当箇所ののソースコードを読んでみると、.on()に集約されているのが分かります。
(jquery.1.7.2.js 3754〜3865行あたり)

jQuery.fn.extend({

  on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
    var origFn, type;

    // Types can be a map of types/handlers
    if ( typeof types === "object" ) {
      // ( types-Object, selector, data )
      if ( typeof selector !== "string" ) { // && selector != null
        // ( types-Object, data )
        data = data || selector;
        selector = undefined;
      }
      for ( type in types ) {
        this.on( type, selector, data, types[ type ], one );
      }
      return this;
    }

    if ( data == null && fn == null ) {
      // ( types, fn )
      fn = selector;
      data = selector = undefined;
    } else if ( fn == null ) {
      if ( typeof selector === "string" ) {
        // ( types, selector, fn )
        fn = data;
        data = undefined;
      } else {
        // ( types, data, fn )
        fn = data;
        data = selector;
        selector = undefined;
      }
    }
    if ( fn === false ) {
      fn = returnFalse;
    } else if ( !fn ) {
      return this;
    }

    if ( one === 1 ) {
      origFn = fn;
      fn = function( event ) {
        // Can use an empty set, since event contains the info
        jQuery().off( event );
        return origFn.apply( this, arguments );
      };
      // Use same guid so caller can remove using origFn
      fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
    }
    return this.each( function() {
      jQuery.event.add( this, types, fn, data, selector );
    });
  },

  one: function( types, selector, data, fn ) {
    return this.on( types, selector, data, fn, 1 );
  },

  bind: function( types, data, fn ) {
    return this.on( types, null, data, fn );
  },

  live: function( types, data, fn ) {
    jQuery( this.context ).on( types, this.selector, data, fn );
    return this;
  },

  delegate: function( selector, types, data, fn ) {
    return this.on( types, selector, data, fn );
  }
});

.on()の最後に「this.each(function(){jQuery.event.add();})」をreturnで返しているので、さらにその辺りを見ると
(jquery.1.7.2.js 2901〜3017行あたり)

/*
 * Helper functions for managing events -- not part of the public interface.
 * Props to Dean Edwards' addEvent library for many of the ideas.
 */
jQuery.event = {

  add: function( elem, types, handler, data, selector ) {

    var elemData, eventHandle, events,
      t, tns, type, namespaces, handleObj,
      handleObjIn, quick, handlers, special;

    // Don't attach events to noData or text/comment nodes (allow plain objects tho)
    if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
      return;
    }

    // Caller can pass in an object of custom data in lieu of the handler
    if ( handler.handler ) {
      handleObjIn = handler;
      handler = handleObjIn.handler;
      selector = handleObjIn.selector;
    }

    // Make sure that the handler has a unique ID, used to find/remove it later
    if ( !handler.guid ) {
      handler.guid = jQuery.guid++;
    }

    // Init the element's event structure and main handler, if this is the first
    events = elemData.events;
    if ( !events ) {
      elemData.events = events = {};
    }
    eventHandle = elemData.handle;
    if ( !eventHandle ) {
      elemData.handle = eventHandle = function( e ) {
        // Discard the second event of a jQuery.event.trigger() and
        // when an event is called after a page has unloaded
        return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
          jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
          undefined;
      };
      // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
      eventHandle.elem = elem;
    }

    // Handle multiple events separated by a space
    // jQuery(...).bind("mouseover mouseout", fn);
    types = jQuery.trim( hoverHack(types) ).split( " " );
    for ( t = 0; t < types.length; t++ ) {

      tns = rtypenamespace.exec( types[t] ) || [];
      type = tns[1];
      namespaces = ( tns[2] || "" ).split( "." ).sort();

      // If event changes its type, use the special event handlers for the changed type
      special = jQuery.event.special[ type ] || {};

      // If selector defined, determine special event api type, otherwise given type
      type = ( selector ? special.delegateType : special.bindType ) || type;

      // Update special based on newly reset type
      special = jQuery.event.special[ type ] || {};

      // handleObj is passed to all event handlers
      handleObj = jQuery.extend({
        type: type,
        origType: tns[1],
        data: data,
        handler: handler,
        guid: handler.guid,
        selector: selector,
        quick: selector && quickParse( selector ),
        namespace: namespaces.join(".")
      }, handleObjIn );

      // Init the event handler queue if we're the first
      handlers = events[ type ];
      if ( !handlers ) {
        handlers = events[ type ] = [];
        handlers.delegateCount = 0;

        // Only use addEventListener/attachEvent if the special events handler returns false
        if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
          // Bind the global event handler to the element
          if ( elem.addEventListener ) {
            elem.addEventListener( type, eventHandle, false ); // <================ ココでElementに対してBindしてる

          } else if ( elem.attachEvent ) {
            elem.attachEvent( "on" + type, eventHandle );
          }
        }
      }

      if ( special.add ) {
        special.add.call( elem, handleObj );

        if ( !handleObj.handler.guid ) {
          handleObj.handler.guid = handler.guid;
        }
      }

      // Add to the element's handler list, delegates in front
      if ( selector ) {
        handlers.splice( handlers.delegateCount++, 0, handleObj );
      } else {
        handlers.push( handleObj );
      }

      // Keep track of which events have ever been used, for event optimization
      jQuery.event.global[ type ] = true;
    }

    // Nullify elem to prevent memory leaks in IE
    elem = null;
  },

2988行あたりに「elem.addEventListener( type, eventHandle, false );」というのを見つけることができます。
対象元となるエレメントである$('#delegate')や$('#on')、$('#one')にも空のclickイベントがバインドされるとわかりました。
Chromeのデベロッパーツールでも確認することができます。

対処方法

変な領域までタップで反応しないようにするには、.bind()メソッドなどでバインドする方法に変更するのが手っ取り早いです。
後は、簡易的に対処する方法があります。webkitには「-webkit-tap-highlight-color」というCSSプロパティがあるので、「-webkit-tap-highlight-color: rgba(0,0,0,0)」と指定することにより一見タップイベントに反応しなくなったようにみせることができます。
ただし「-webkit-tap-highlight-color」はAndroidでの実装がまちまちだったりするので、あまりおすすめできません。
「-webkit-tap-highlight-color」について詳しくはAppleのSafari Web Content Guide - Customizing Style SheetsのHighlighting Elementsの項を参照してください。


テスト用に作ったページでは、最下部に設置したボタンを押すと「-webkit-tap-highlight-color」のスタイルが当たるようにしています。
iPhoneではうまくいっているように見える感じになりますが、Androidは機種によってはおかしな挙動になることが分かりますので、そちらも見てみてください。