スマートフォンブラウザの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は機種によってはおかしな挙動になることが分かりますので、そちらも見てみてください。

