HOME > 開発者向けBLOG > Sencha Blog >  Ext JS におけるイベント委譲とジェスチャー

Technology Note 開発者向けBLOG

Sencha Blog

Ext JS におけるイベント委譲とジェスチャー

こんにちは、ゼノフィnakamuraです。

この記事は、US Sencha社ブログ Delegated Events and Gestures in Ext JS 5 を翻訳したものです。

はじめに

バージョン5以前では、Ext JSは伝統的なマウス入力をするデスクトップ機器に向けに設計されていました。 バージョン5 からは、タッチ入力のサポートを追加しました。 そのため、Ext JSはより多くの機器、特にタブレットやタッチパネル付きのノートパソコン、で利用できます。 この変更の効果はフレームワークのユーザーの皆様には明らかだと思いますが、内部的になにが行われているのかを理解するのも役立ちます。この記事では、このフレームワークがどのようにタッチイベントをハンドリングし、機器間でのタッチイベントの違いを標準化しているかを説明します。

Ext JSのジェスチャー

おそらく Ext JS 5のイベントシステムに追加された最も魅力的なものは “gesture” イベントです。 Sencha Touch のイベントシステムが Ext JS 5 の新しいイベントシステムの基となっているため、Sencha Touch のユーザーはたくさんのジェスチャーを既に身近に感じるかもしれません。まだご存知でない方のために言いますと、ジェスチャーは低レベルのブラウザイベントから合成された複雑なイベントです。”drag”, “swipe”, “longpress”, “pinch”, “rotate”, “tap”などがあります。Ext JS 5 ではタッチジェスチャーをさらに一歩進化させたので、マルチインプットデバイスの場合には、タッチ入力とともにマウス入力も (または両方で) 操作できるようになっています。

ブラウザから見ると、三つの基本的なイベント (start, move, end) があります。 これはマウスやタッチパネルでページとやりとりしているときに発火します。 フレームワークはいつジェスチャーが行われたかを判断するために、この三つのイベントの順序とタイミングをモニタリングしています:

ブラウザ Start Move End
デスクトップブラウザ mousedown mousemove mouseup
モバイル Webkit touchstart touchmove touchend
IE10 MSPointerDown MSPointerMove MSPointerUp
IE11 pointerdown pointermove pointerup

フレームワークがジェスチャーが行われたと判断したら、リッスンしているエレメントにジェスチャーイベントを送ります。 ジェスチャーイベントのリッスンは、DOMイベントの場合と同じです。例えば:

myElement.on('drag', myFunction);

次のシングルタッチジェスチャーは、入力デバイスとして使っているのは何なのか (マウス、タッチ) とは関係なく、サポートしている機器、ブラウザ全てで動作します:

ジェスチャー イベント
Tap tap, tapcancel
DoubleTap singletap, doubletap
LongPress longpress
Drag dragstart, drag, dragend, dragcancel
Swipe swipe, swipestart, swipecancel
EdgeSwipe edgeswipestart, edgeswipe, edgeswipecancel

次の「マルチタッチ」ジェスチャーは、タッチ操作が可能なブラウザが、タッチパネルがある端末で動作している場合には動作します。

ジェスチャー イベント
Pinch pinchstart, pinch, pinchend, pinchcancel
Rotate rotatestart, rotate, rotateend, rotatecancel

委譲イベントモデル

Ext JS 5 のイベントシステムでは、些細ですが重要な変更が行なわれました。 DOM リスナーを直接アタッチするのをやめ、 「委譲(delegated)」イベントモデルに変わったのです。 このため、各イベントの種類 (“mousedown”, “touchstart” など) で、DOM階層の頂点 (window オブジェクト) に一つのリスナーがアタッチされます。 DOMエレメントがイベントを発火したら、ハンドリングされるまえに頂点まで遡上します。 内部的には、イベントシステムはターゲットエレメントからDOMの階層をさかのぼりながら (必要であればイベントハンドラーも同時に送信して) イベント伝播をエミュレートする必要があるので、少し複雑になります。このアプローチはむだに複雑なように見えますが、いくつか重要な長所があります:

  1. ジェスチャーがいつ起こったのかを認識するためには、委譲イベントモデルは重要です。イベントシステムはサポートされているジェスチャーが起きたかどうか検出するために、いくつかのキーイベントのタイミングと順序を継続的にモニタリングします。 その後は、正しい順番で発生したネイティブイベントを合成したら、ジェスチャーイベントがネイティブイベントとともに、シームレスでディスパッチされます。
  2. アタッチされている DOM リスナーの数も大きく削減しますので、メモリーの利用を改善し、DOM リスナーを削除するのは一つのポイントになります。これは window がアンロードするときに整理するのがよりシンプルになるので、古いブラウザのメモリーリークの可能性を削減する利点もあります。
  3. これは古いブラウザでイベントの「トップダウン」の伝播 (capture) を可能とします。 IE8は addEventListener() と “useCapture” オプションに対応していないため、直接アタッチされている DOM イベントはボトムアップの bubbling モデルで伝播することしかできません。 委譲イベントモデルでは、人工の伝播ルーチンを使って、”bubble” と “capture” 両方のイベント伝播を実装して、この問題を避ける方法を提供しています。 ユーザーはイベントを追加する時に、単に “capture” オプションを渡すだけでキャプチャーリスナーを追加することができて、イベントハンドラーは全てのブラウザやデバイスでトップダウンの順番でディスパッチされます。例えば:
1
2
3
4
myElement.on({
  click: someFunction,
  capture: true
});

委譲モデルの潜在的課題

フレームワークの根本的な変更では、既存のコードと適合しなくなる可能性があります。 新しいイベントシステムに関連ては、既存のコードはおよそ次の二種類に分けられます。

  1. イベントリスナーを追加する際に Ext JS イベントシステムAPI (例えば Ext.Element#addListener() や Ext.Element#on()) だけ利用するアプリケーションコードは、変化があったことを感知しません。アプリケーションコードがリスナーを追加するために Ext JS API だけを使っている場合には、新しいイベントシステムは Ext JS 4 イベントシステムと100% の後方互換性があります。
  2. Ext JS を他の JavaScript ライブラリと組み合わせているアプリケーションコード、または DOM API を使ってエレメントに直接イベントを追加しているコードは、委譲イベントシステムの切り替えで悪影響がある可能性があります。これは直に追加されているイベントハンドラーのタイミングと、委譲された相手のタイミングとは変わってきます。もしアプリケーションコードがイベントハンドラーが決まった順序で動作することを期待している場合には、いくつかの問題が発生するかもしれませんが、イベントハンドラーがあるイベントの 伝播を止めようとする時 に明らかになります。ここで覚えておくべきのルールは二つあります:
    1. 委譲されたリスナーは stopPropagation() を呼び出して、委譲したイベントシステムをエミュレートした伝播を止めますが、直に追加されたリスナーの発火を防ぐことはできません。その理由は、委譲したリスナーが処理された後には、ネイティブイベントが既に DOM の頂点までbubbleしているため、直に追加されたリスナーの発火を防ごうとしても手遅れだからです。
    2. もし直に追加された DOM リスナーが stopPropagation() を呼び出すと、 DOM 内で下のエレメントも含めて、全ての委譲されたリスナーの発火を止めます。この理由は、stopPropagation() を直に追加されているイベントで使うと、ネイティブのイベントを上まで bubble するのを止めるので、それが委譲イベントシステムでは扱われなくなります。ジェスチャーは DOM の頂点で管理されているため、ジェスチャー認識も無効になる可能性があります。

もし委譲モデルが必要ない場合には、オプションオブジェクトで簡単なフラグを使うと、リスナーを委譲モデルから解放できます:

しかし、委譲イベントモデルから解放する時には注意が必要です。上で説明したような直に追加された DOM リスナーと同じ悪影響が発生する可能性があります (特に stopPropagation が関係している場合)。

イベント正常化

新しいイベントシステムの大きな目的は、アプリケーションをほとんどアップグレードしなくても、既存在の Ext アプリケーションがタブレットとタッチパネル付きのノートパソコンで動作できることです。これを実現するには、内部的にフレームワークは基本的なイベントの標準化を行います。mousedown や click のようなマウスイベントにリスナーがリクエストされる時に、フレームワークはよく似た touch イベント、または pointer イベントを追加します (端末がそのイベントに対応している場合には) 。例えば、あるアプリケーションが mousedown リスナーを追加するとすると。

myElement.on('mousedown', someFunction);

Mobile Safariでは、イベントシステムは次のように解釈されます:

myElement.on('touchstart', someFunction);

しかし、IE11で動作している場合には、その同じリスナーは次のように解釈されます:

myElement.on('pointerdown', someFunction);

このため、タッチパネル操作はマウス操作とほぼ同じように動作します。

次のマウスイベントは、タッチ可能な端末で動作している時に touch または pointer イベントに直接解釈されます:

  • mousedown -> touchstart または pointerdown
  • mousemove -> touchmove または pointermove
  • mouseup -> touchend または pointerup
  • click -> tap
  • dblclick -> doubletap

しかし、いくつかのマウスイベントに関しては、タッチ世界にぴったりとした代役がいないものがあります。あるアプリケーションが次のイベントに頼っている場合には、開発者がタッチ入力の場合にその機能はどう実装するのか自分で決める必要があります:

  • mouseover
  • mouseout
  • mouseenter
  • mouseleave

ほとんどのアプリケーションではイベント標準化は役に立ちますが、使用したくない場合もあると思います。Ext JS 5 はこのために新しい “translate” イベントオプションを提供しています。

1
2
3
4
myElement.on({
    mousedown: myFunction,
    translate: false
});

“translate” オプションを false にセットすると、イベントシステムがこのリスナーにイベント標準化を実行しないようになりますので、本当の mousedown が起きた時だけハンドラー関数が呼び出されます。

最後に

Ext JS は長い間 HTML5 デスクトップアプリケーションの最先端のフレームワークでしたが、現在デスクトップとモバイルの区切りは徐々に曖昧になってきました。この新しいジェスチャーとイベント標準化が付いた Ext JS 5 イベントシステムは、今までのデスクトップスタイルのデバイスとともに、タッチ操作できるデバイスやブラウザの新たな世界でも秀でたものになりました。

PAGETOP