Knockout.jsで'touchend'のカスタムバインディング


スマフォ用サイトでclickイベントを使用すると、モサっとした感じになります。
この現象については、clickイベントには待ち時間が存在するからで、スマフォサイトの場合はtouchイベントを使う事で解決する事になると思います。

しかしながら、今のところKnockout.jsにはtouchstarttouchendといったバインディングは存在しません。
touchendイベントを指定したい場合等は、eventバインディングを使用します。

event-binding.html
<div data-bind="with: app">
  <button type="button" data-bind="event: {touchend: foo}">TOUCH!!</button>
</div>

<script>
var ViewModel = (function(){
  function ViewModel(){}

  ViewModel.prototype.foo = function(){
    alert('タッチされました。');
  };
})();

ko.applyBindings({
  app: new ViewModel()
});
</script>

これでtouchendイベントを紐付ける事が出来ました。
めでたし・めでたし

・・・ではありません

touchendは画面をタッチしてスクロールした時も、指を離した瞬間に発生してしまいます。
これを防ぐために、touchstarttouchendの組み合わせで制御したりしますが、上記の様にeventバインディングで対応するとなると、これはもう大変な作業になってしまいます。

そこで、カスタムバインディングを使用する方法を考えてみます。

touchend-binding.html
<div data-bind="with: app">
  <button type="button" data-bind="touchend: foo">TOUCH!!</button>
</div>

<script>
var ViewModel = (function(){
  function ViewModel(){}

  ViewModel.prototype.foo = function(){
    alert('タッチされました。');
  };
})();

ko.applyBindings({
  app: new ViewModel()
});

// touchendのカスタムバインディング
ko.bindingHandlers['touchend'] = {
  'init': function(element, valueAccessor, allBindings, viewModel, bindingContext){
    var $el = $(element);
    var srcYOffset = null;

    // タッチスタート時に、元々のY座標を取得
    $el.on("touchstart", function(){
      srcYOffset = window.pageYOffset;
    });

    // タッチエンド
    $el.on("touchend", function(){
      var destYOffset = window.pageYOffset;

      if (srcYOffset === destYOffset) {
        var handlerFunction = valueAccessor();

        viewModel = bindingContext['$data'];

        handlerReturnValue = handlerFunction.apply(viewModel, arguments);
      }
    });
  }
};
</script>

touchend用のカスタムバインディングの中で、スクロールが発生している場合を判断する様に制御を入れました。
これでdata-bind="touchend: foo"といった様な記述だけで、スクロール制御がされたtouchendイベントが使用できます。

追記(答えなし)

上記のコードでちょっと気になっているのが、$elに直接イベントをバインディングしているところです。
これだと、イベントの登録数が増える事が想定されるので、改善案を考えたいところです。