読者です 読者をやめる 読者になる 読者になる

GeekFactory

int128.hatenablog.com

Knockout.js で Drag and Drop

javascript knockoutjs

Knockout.js で jQuery UI の Draggable and Droppable を利用する方法を説明します。

まず、新しい binding を定義します。

draggable binding
初期化時に $(element).draggable() を実行し、要素をドラッグ可能にする。
droppable binding
初期化時に $(element).droppable() を実行し、要素に対してドロップ可能にする。$(element).droppable() ではドロップ時のハンドラを指定する。

整理すると、 draggable な要素を droppable な要素にドロップできるようになります。ドロップ時の動作は droppable で設定します。ややこしいです。

[draggable な要素] → [droppable な要素]

ドロップ時のハンドラでは draggable な要素は取得できますが、draggable な要素にバインドされている ViewModel は直接取得できません*1。そこで、ドロップ時のハンドラで draggable な要素にイベントを投げることで、 draggable な要素にバインドされている ViewModel に振る舞いを記述できます。

具体的な流れを示します。

  1. ユーザが draggable な要素をドラッグする。
  2. ユーザが draggable な要素を droppable な要素にドロップする。
  3. droppable な要素で drop イベントが発火し、イベントハンドラが実行される。
  4. イベントハンドラは draggable な要素(引数から取得可能)に対して独自イベントを発火させる。イベントの引数として自身にバインドされている ViewModel を渡す。
  5. draggable な要素で独自イベントが発火し、イベントにバインドされているメソッドが実行される。メソッドは引数として droppable の ViewModel を受け取る。

このパターンは、ドロップ時の振る舞いを draggable に記述する方が自然な場合に適しています。例えば、カレンダーに予定がドロップされた時、予定のプロパティを変更するなど。

コード例を載せておきます。

<!-- ドラッグされる要素 -->
<div data-bind="draggable: true, event: {dropped: dropped}" class="hoge">
  ぬるぽ
</div>

<!-- ドロップ先 -->
<div data-bind="droppable: true" data-drop-accept=".hoge" data-drop-hover-class="dropping">
  ここにドロップしてね
</div>
/**
 * Drag and drop binding.
 */
ko.bindingHandlers.draggable = {
  init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
   $(element).draggable();
  },
  update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
   if (valueAccessor()) {
    $(element).draggable('enable');
   } else {
    $(element).draggable('disable');
   }
  }
};
ko.bindingHandlers.droppable = {
  init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
   var $element = $(element);
   $element.droppable({
    accept: $(element).data('drop-accept'),
    hoverClass: $(element).data('drop-hover-class'),
    tolerance: 'pointer',
    drop: function (e, ui) {
     // reset position
     ui.draggable.css({top: 0, left: 0});
     // trigger event
     ui.draggable.trigger('dropped', [viewModel, e, ui]);
    }
   });
  },
  update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
   if (valueAccessor()) {
    $(element).droppable('enable');
   } else {
    $(element).droppable('disable');
   }
  }
};

Drag and Drop はいろんなケースを考慮すると一般化しづらいと思います。このエントリは一つの解と考えていただければと思います。

*1:$.data() を使って要素と ViewModel の対応を覚えておけば取得できそう。