JavaScript

JavaScriptでTouchEvents(Level-2)を擬似発火する

More than 1 year has passed since last update.

TouchEvents Level-2では、JavaScriptからのTouchEvent擬似発火が簡単になった。
さっそく試してみよう。

注意: まだ仕様はEditor's Draftなので変わる可能性あり。

対応ブラウザ

MDNによると、

Browser version
Chrome 48.0
Firefox ?
IE 12.0
Opera 15.0
Safari ?

?ってなにこれ。
試してみたら、Firefoxでは使えなかった。ジャンク屋の動作未確認のようなもんだな!
まあ、今回はChromeで試すだけなので問題ない。

TouchEvent Level-2以前の世界

v2以前のタッチの擬似発火は本当につらいものだった。
初期化用のinitTouchEvent関数には15個くらい引数が必要となる。(座標指定も必要な割には無視される。)
その引数の順番はブラウザごとに異なるうえに、リファレンスなんて気の利いたものはありゃしないのだ。

記事ではChromeの仕様をChromiumソースコードから読み取っている。
ソースコードはドキュメント!みんなも困ったらブラウザのソース読もう!
ChromeもMSDNやMDN的なものあったらいいのに!

なお、initTouchEventがあまりにもカオスなので、W3Cも共通化を諦めたようだ。

ですよねー。

TouchEventコンストラクタ

さてv2では、TouchEventにコンストラクタがついてる。やったぜ。

https://w3c.github.io/touch-events/#touchevent-interface

WebIDL
dictionary TouchEventInit : EventModifierInit {
             sequence<Touch> touches = [];
             sequence<Touch> targetTouches = [];
             sequence<Touch> changedTouches = [];
};

[Constructor(DOMString type, optional TouchEventInit eventInitDict)]
interface TouchEvent : UIEvent {
    readonly        attribute TouchList touches;
    readonly        attribute TouchList targetTouches;
    readonly        attribute TouchList changedTouches;
    readonly        attribute boolean   altKey;
    readonly        attribute boolean   metaKey;
    readonly        attribute boolean   ctrlKey;
    readonly        attribute boolean   shiftKey;
};

DOMStringはStringと同じ。(ECMAScriptでは)

TouchEventInitは定義通りだが結構階層が深い。

EventInitでbubblesとcancelableがデフォルトfalseになっているのにびっくり。
擬似発火するときはそりゃもちろんバブリングしてほしいから、trueを指定してやる必要がある。

肝心のタッチ指定は配列で渡すだけというお手軽さ。TouchListとやらを生成する必要もない。

// タッチイベント生成 (touch1-3の生成方法は後述)
const event = new TouchEvent('touchstart', {
  touches: [touch1, touch2, touch3],
  targetTouches: [touch2, touch3],
  changedTouches: [touch3],
  bubbles: true,
  cancelable: true
});
element.dispatchEvent(event);

各タッチの意味は以下のとおり。

  • touches: 画面全ての指リスト
  • targetTouches: 同じ要素上の指リスト
  • changedTouches: 今回発火したイベント(指が触れたなど)に関連する指リスト

Touchコンストラクタ

https://w3c.github.io/touch-events/#touch-interface

WebIDL
dictionary TouchInit {
    required long        identifier;
    required EventTarget target;
             double      clientX = 0;
             double      clientY = 0;
             double      screenX = 0;
             double      screenY = 0;
             double      pageX = 0;
             double      pageY = 0;
             float       radiusX = 0;
             float       radiusY = 0;
             float       rotationAngle = 0;
             float       force = 0;
};

[Constructor(TouchInit touchInitDict)]
interface Touch {
    readonly        attribute long        identifier;
    readonly        attribute EventTarget target;
    readonly        attribute double      screenX;
    readonly        attribute double      screenY;
    readonly        attribute double      clientX;
    readonly        attribute double      clientY;
    readonly        attribute double      pageX;
    readonly        attribute double      pageY;
    readonly        attribute float       radiusX;
    readonly        attribute float       radiusY;
    readonly        attribute float       rotationAngle;
    readonly        attribute float       force;
};

こいつもコンストラクタがついてる。
引数はなんとオブジェクト1つだけ。必須プロパティも2つだけ。
シンプルでいいね。まあ普通こうするよなあ。

タッチの場合、指を識別するためのidentifierプロパティが生えている。
他にもいろいろプロパティが生えてて面白い。

  • radiusX, radiusY: 指の大きさ
  • rotationAngle: 指の回転角
  • force: 押した強さ

擬似発火

これでTouchを生成してTouchEvent擬似発火ができる。
ちょっと長いけどこんな感じ。エラー処理はなし。

es6
class TouchEmulator {
  constructor(){
    this.touches = []; // 全タッチを保持
  }

  touchstart(id, point){
    const target = document.elementFromPoint(point.x, point.y);
    const touch = this.createTouch(id, target, point);

    // touchesに追加
    this.touches.push(touch);

    this.triggerTouchEvent('touchstart', touch);
  }

  touchmove(id, point){
    const index = this.touches.findIndex(t => t.identifier === id);
    const target = this.touches[index].target;

    const touch = this.createTouch(id, target, point);

    // touchesを更新
    this.touches[index] = touch;

    this.triggerTouchEvent('touchmove', touch);
  }

  touchend(id, point){
    const target = this.touches.find(t => t.identifier === id).target;

    const touch = this.createTouch(id, target, point);

    // touchesから除去
    this.touches = this.touches.filter(t => t.identifier !== id);

    this.triggerTouchEvent('touchend', touch);    
  }

  // Touchをxとyから生成する
  createTouch(identifier, target, point){
    return new Touch({
      identifier,
      target,
      clientX: point.x,
      clientY: point.y,
      pageX: point.x + window.pageXOffset,
      pageY: point.y + window.pageYOffset,
      radiusX: 10,
      radiusY: 10,
      force: 1
    });  
  }

  // TouchEventを作って発火する
  triggerTouchEvent(name, touch) {
    // targetが同じTouchを取り出す
    const targetTouches = this.touches.filter(t => t.target === touch.target);
    const event = new TouchEvent(name, {
      touches: this.touches,
      targetTouches,
      changedTouches: [touch],
      bubbles: true, // これがないとバブリングしない
      cancelable: true,
      view: window
    });
    touch.target.dispatchEvent(event);
  }
}

追記: ソース一部修正。jsdo.itでサンプル書いてます。

document.elementFromPoint(x,y)で座標から要素を取得している。
こうやって使う。

var emulator = new TouchEmulator();
elmulator.touchstart(1, {x: 100, y: 200});
elmulator.touchmove(1, {x: 120, y: 200});
elmulator.touchstart(2, {x: 200, y: 250});
elmulator.touchend(1, {x: 150, y: 220});
elmulator.touchend(2, {x: 200, y: 250});

こいつの一般的な用途は知らない。業務で必要になったのだよ。

その他メモ

  • TouchEventはtouchmove,touchendイベントもtouchstartと同じ要素で発火する。
  • アプリ側でスクリプトによる擬似発火イベントかどうかを見分けたいなら、Event.isTrustedが使える。
  • タッチイベント系のライブラリがうまく騙せなくて困っている。