2024年1月27日

ポインターイベント

ポインターイベントは、マウス、ペン/スタイラス、タッチスクリーンなど、さまざまなポインティングデバイスからの入力を処理する最新の手段です。

簡単な歴史

全体像と、他のイベントタイプの中でポインターイベントがどのような位置付けにあるのかを理解するために、簡単に概要を説明します。

  • 昔は、マウスイベントしかありませんでした。

    その後、タッチデバイス、特に携帯電話やタブレットが普及しました。既存のスクリプトが動作するように、それらはマウスイベントを生成しました(そして今も生成しています)。たとえば、タッチスクリーンをタップすると`mousedown`が生成されます。そのため、タッチデバイスはWebページでうまく機能しました。

    しかし、タッチデバイスはマウスよりも多くの機能を備えています。たとえば、複数のポイントを同時にタッチする(「マルチタッチ」)ことができます。しかし、マウスイベントには、そのようなマルチタッチを処理するために必要なプロパティがありません。

  • そこで、タッチ固有のプロパティを持つ`touchstart`、`touchend`、`touchmove`などのタッチイベントが導入されました(ポインターイベントの方が優れているため、ここでは詳細には説明しません)。

    それでも、ペンなど、独自の機能を持つ他のデバイスが多数あるため、十分ではありませんでした。また、タッチイベントとマウスイベントの両方をリッスンするコードを書くのは面倒でした。

  • これらの問題を解決するために、新しい標準であるポインターイベントが導入されました。これは、あらゆる種類のポインティングデバイスに対して単一のイベントセットを提供します。

現在、ポインターイベントレベル2の仕様はすべての主要ブラウザでサポートされていますが、新しいポインターイベントレベル3は開発中で、ポインターイベントレベル2とほぼ互換性があります。

Internet Explorer 10などの古いブラウザ、またはSafari 12以前のブラウザ向けに開発していない限り、もはやマウスイベントやタッチイベントを使用する意味はありません。ポインターイベントに切り替えることができます。

そうすれば、コードはタッチデバイスとマウスデバイスの両方でうまく機能します。

とはいえ、ポインターイベントを正しく使用し、予期しない事態を避けるために知っておくべき重要な点がいくつかあります。この記事では、それらについて説明します。

ポインターイベントタイプ

ポインターイベントの名前はマウスイベントと似ています

ポインターイベント 同様のマウスイベント
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave
pointercancel -
gotpointercapture -
lostpointercapture -

ご覧のとおり、すべての`mouse<event>`には、同様の役割を果たす`pointer<event>`があります。また、対応する`mouse...`がない3つの追加のポインターイベントがあります。これらについてはすぐに説明します。

コードで`mouse<event>`を`pointer<event>`に置き換える

コードで`mouse<event>`イベントを`pointer<event>`に置き換えることができ、マウスで正常に動作し続けることが期待できます。

タッチデバイスのサポートも「魔法のように」向上します。ただし、CSSの一部の場所で`touch-action: none`を追加する必要がある場合があります。これについては、以下の`pointercancel`のセクションで説明します。

ポインターイベントプロパティ

ポインターイベントは、`clientX/Y`、`target`など、マウスイベントと同じプロパティに加えて、他にもいくつかプロパティがあります。

  • `pointerId` - イベントを引き起こしているポインターの一意の識別子。

    ブラウザによって生成されます。スタイラスとマルチタッチを備えたタッチスクリーンなど、複数のポインターを処理できます(例は後述します)。

  • `pointerType` - ポインティングデバイスタイプ。「mouse」、「pen」、または「touch」のいずれかの文字列である必要があります。

    このプロパティを使用して、さまざまなポインタータイプに異なる方法で反応できます。

  • `isPrimary` - プライマリポインター(マルチタッチの最初の指)の場合は`true`です。

一部のポインターデバイスは、接触面積と圧力を測定します。たとえば、タッチスクリーン上の指の場合、 daarvoor 追加のプロパティがあります

  • `width` - ポインター(例:指)がデバイスに接触する領域の幅。サポートされていない場合、たとえばマウスの場合、常に`1`です。
  • `height` - ポインターがデバイスに接触する領域の高さ。サポートされていない場合、常に`1`です。
  • `pressure` - ポインターの先端の圧力。0〜1の範囲です。圧力をサポートしていないデバイスの場合は、`0.5`(押されている)または`0`のいずれかである必要があります。
  • `tangentialPressure` - 正規化された接線方向の圧力。
  • `tiltX`、`tiltY`、`twist` - ペンが表面に対してどのように配置されているかを表すペン固有のプロパティ。

これらのプロパティはほとんどのデバイスでサポートされていないため、めったに使用されません。必要に応じて、仕様書で詳細を確認できます。

マルチタッチ

マウスイベントがまったくサポートしていないことの1つは、マルチタッチです。ユーザーは、携帯電話やタブレットの複数の場所を同時にタッチしたり、特別なジェスチャを実行したりできます。

ポインターイベントでは、`pointerId`プロパティと`isPrimary`プロパティを使用してマルチタッチを処理できます。

ユーザーがタッチスクリーンの1か所をタッチし、次に別の指を別の場所に置いた場合に何が起こるかを次に示します。

  1. 最初の指のタッチ時
    • `isPrimary = true`といくつかの`pointerId`を持つ`pointerdown`。
  2. 2番目の指とそれ以上の指の場合(最初の指がまだタッチしていると仮定)
    • `isPrimary = false`と、指ごとに異なる`pointerId`を持つ`pointerdown`。

注:`pointerId`はデバイス全体ではなく、タッチしている指ごとに割り当てられます。5本の指を使用して画面に同時に触れると、5つの`pointerdown`イベントが発生し、それぞれにそれぞれの座標と異なる`pointerId`が割り当てられます。

最初の指に関連付けられたイベントは、常に`isPrimary = true`になります。

`pointerId`を使用して、複数のタッチしている指を追跡できます。ユーザーが指を動かしてから離すと、`pointerdown`と同じ`pointerId`を持つ`pointermove`イベントと`pointerup`イベントが発生します。

`pointerdown`イベントと`pointerup`イベントをログに記録するデモを次に示します。

注:`pointerId / isPrimary`の違いを実際に確認するには、電話やタブレットなどのタッチスクリーンデバイスを使用する必要があります。マウスなどのシングルターチデバイスの場合、すべてのポインターイベントで、`isPrimary = true`の同じ`pointerId`が常に使用されます。

イベント:pointercancel

`pointercancel`イベントは、進行中のポインターインタラクションがあり、その後、ポインターイベントが生成されないように中止される原因となる何かが発生した場合に発生します。

そのような原因は次のとおりです

  • ポインターデバイスのハードウェアが物理的に無効になっている。
  • デバイスの向きが変更された(タブレットが回転した)。
  • ブラウザは、インタラクションをマウスジェスチャまたはズームアンドパンアクションまたはその他の何かと見なし、独自に処理することを決定しました。

実際の例で`pointercancel`をデモンストレーションして、それがどのように影響するかを確認します。

マウスイベントによるドラッグアンドドロップの記事の冒頭と同様に、ボールのドラッグアンドドロップを実装しているとします。

ユーザーのアクションと対応するイベントの流れは次のとおりです。

  1. ユーザーは画像を押してドラッグを開始します
    • `pointerdown`イベントが発生します
  2. 次に、ポインターを動かし始めます(画像をドラッグします)
    • `pointermove`が数回発生する可能性があります
  3. そして、驚きが起こります!ブラウザには画像のネイティブドラッグアンドドロップサポートがあり、それが開始されてドラッグアンドドロッププロセスを引き継ぎ、`pointercancel`イベントを生成します。
    • ブラウザは、画像のドラッグアンドドロップを独自に処理するようになりました。ユーザーは、ボールの画像をブラウザからメールプログラムまたはファイルマネージャにドラッグすることもできます。
    • これ以上`pointermove`イベントは発生しません。

そのため、問題はブラウザがインタラクションを「ハイジャック」することです。`pointercancel`は「ドラッグアンドドロップ」プロセスの開始時に発生し、`pointermove`イベントはそれ以上生成されません。

`textarea`にポインターイベント(`up/down`、`move`、`cancel`のみ)のログを記録するドラッグアンドドロップのデモを次に示します。

独自のドラッグアンドドロップを実装したいので、ブラウザにそれを引き継がないように指示しましょう。

`pointercancel`を回避するために、デフォルトのブラウザアクションを防ぎます。

2つのことを行う必要があります

  1. ネイティブのドラッグアンドドロップが発生しないようにする
  2. タッチデバイスの場合、他のタッチ関連のブラウザアクション(ドラッグアンドドロップ以外)があります。それらに関する問題も回避するには
    • CSSで`#ball {touch-action:none}`を設定して、それらを防ぎます。
    • そうすれば、コードはタッチデバイスで動作し始めます。

これを行った後、イベントは意図したとおりに動作し、ブラウザはプロセスをハイジャックせず、`pointercancel`を発行しません。

このデモはこれらの行を追加します

ご覧のとおり、`pointercancel`はもうありません。

これで、ボールを実際に動かすコードを追加できるようになり、ドラッグアンドドロップはマウスデバイスとタッチデバイスで機能します。

ポインターキャプチャ

ポインターキャプチャは、ポインターイベントの特別な機能です。

このアイデアは非常にシンプルですが、他のイベントタイプには同様のものが存在しないため、最初は奇妙に思えるかもしれません。

主なメソッドは

  • elem.setPointerCapture(pointerId) – 指定された pointerId を持つイベントを elem にバインドします。呼び出し後、同じ pointerId を持つすべてのポインターイベントは、ドキュメント内で実際に発生した場所に関係なく、elem をターゲット(elem で発生した場合と同様)として持ちます。

言い換えれば、elem.setPointerCapture(pointerId) は、指定された pointerId を持つ後続のすべてのイベントを elem にリターゲットします。

バインディングは次の場合に削除されます

  • pointerup または pointercancel イベントが発生した場合、自動的に削除されます。
  • elem がドキュメントから削除された場合、自動的に削除されます。
  • elem.releasePointerCapture(pointerId) が呼び出された場合。

では、これは何の役に立つのでしょうか?実際の例を見てみましょう。

ポインターキャプチャは、ドラッグアンドドロップのようなインタラクションを簡素化するために使用できます。

マウスイベントによるドラッグアンドドロップで説明されているカスタムスライダーの実装方法を思い出してみましょう。

ストリップと内部の「ランナー」(thumb)を表す slider 要素を作成できます

<div class="slider">
  <div class="thumb"></div>
</div>

スタイルを適用すると、次のようになります

そして、説明されているように、マウスイベントを同様のポインターイベントに置き換えた後の動作ロジックは次のとおりです

  1. ユーザーがスライダーの thumb を押すと、pointerdown がトリガーされます。
  2. 次に、ポインターを移動すると、pointermove がトリガーされ、コードによって thumb 要素が移動します。
    • …ポインターが移動すると、スライダーの thumb 要素から外れて、その上または下に移動することがあります。 thumb は、ポインターと位置を合わせながら、厳密に水平方向に移動する必要があります。

マウスイベントベースのソリューションでは、ポインターが thumb の上/下を通過する場合も含め、すべてのポインターの動きを追跡するために、mousemove イベントハンドラーをドキュメント全体に割り当てる必要がありました。

しかし、これは最もクリーンな解決策ではありません。問題の1つは、ユーザーがドキュメント周辺でポインターを移動すると、他の要素でイベントハンドラー(mouseover など)がトリガーされ、まったく無関係なUI機能が呼び出される可能性があり、それは望ましくないことです。

ここで setPointerCapture が活躍します。

  • pointerdown ハンドラーで thumb.setPointerCapture(event.pointerId) を呼び出すことができます。
  • すると、pointerup/cancel までの将来のポインターイベントは thumb にリターゲットされます。
  • pointerup が発生すると(ドラッグが完了すると)、バインディングは自動的に削除されるため、気にする必要はありません。

そのため、ユーザーがドキュメント全体でポインターを移動しても、イベントハンドラーは thumb で呼び出されます。それでも、clientX/clientY などのイベントオブジェクトの座標プロパティは依然として正しいままです。キャプチャは target/currentTarget にのみ影響します。

重要なコードは次のとおりです

thumb.onpointerdown = function(event) {
  // retarget all pointer events (until pointerup) to thumb
  thumb.setPointerCapture(event.pointerId);

  // start tracking pointer moves
  thumb.onpointermove = function(event) {
    // moving the slider: listen on the thumb, as all pointer events are retargeted to it
    let newLeft = event.clientX - slider.getBoundingClientRect().left;
    thumb.style.left = newLeft + 'px';
  };

  // on pointer up finish tracking pointer moves
  thumb.onpointerup = function(event) {
    thumb.onpointermove = null;
    thumb.onpointerup = null;
    // ...also process the "drag end" if needed
  };
};

// note: no need to call thumb.releasePointerCapture,
// it happens on pointerup automatically

完全なデモ

デモでは、onmouseover ハンドラーを使用して現在の日付を表示する追加の要素もあります。

注意:サムをドラッグしている間、この要素にカーソルを合わせても、そのハンドラーはトリガーされ*ません*。

そのため、setPointerCapture のおかげで、ドラッグは副作用から解放されます。

最終的に、ポインターキャプチャには2つの利点があります

  1. ドキュメント全体にハンドラーを追加/削除する必要がなくなるため、コードがクリーンになります。バインディングは自動的に解放されます。
  2. ドキュメントに他のポインターイベントハンドラーがある場合、ユーザーがスライダーをドラッグしている間、ポインターによって誤ってトリガーされることはありません。

ポインターキャプチャイベント

完全を期すために、ここで言及すべきことがもう1つあります。

ポインターキャプチャに関連付けられたイベントは2つあります

  • gotpointercapture は、要素が setPointerCapture を使用してキャプチャを有効にしたときに発生します。
  • lostpointercapture は、キャプチャが解放されたときに発生します。明示的に releasePointerCapture を呼び出すか、pointerup/pointercancel で自動的に解放されます。

まとめ

ポインターイベントを使用すると、マウス、タッチ、ペンイベントを単一のコードで同時に処理できます。

ポインターイベントはマウスイベントを拡張します。イベント名で mousepointer に置き換えることができ、コードがマウスで引き続き機能し、他のデバイスタイプのサポートが向上することを期待できます。

ブラウザが独自にハイジャックして処理することを決定する可能性のあるドラッグアンドドロップや複雑なタッチインタラクションについては、イベントのデフォルトアクションをキャンセルし、使用する要素のCSSで touch-action: none を設定することを忘れないでください。

ポインターイベントの追加機能は次のとおりです

  • pointerIdisPrimary を使用したマルチタッチサポート。
  • pressurewidth/height などのデバイス固有のプロパティ。
  • ポインターキャプチャ:pointerup/pointercancel まで、すべてのポインターイベントを特定の要素にリターゲットできます。

現在、ポインターイベントはすべての主要なブラウザでサポートされているため、特にIE10-とSafari 12-が必要ない場合は、安全に切り替えることができます。これらのブラウザでも、ポインターイベントのサポートを有効にするポリフィルがあります。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHubのissueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code> タグを使用し、複数行の場合は <pre> タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。