interact.jsを使ってみる

interact.jsを使ってみる

interact.jsって?

interact.jsは、ドラッグ&ドロップをサポートしてくれるJavaScriptライブラリです。 マウスでのドラッグ&ドロップだけでなく、リサイズやスナップ、スマホなどのタッチデバイスでの2本指ピンチイン・ピンチアウト・回転といったマルチタッチジェスチャーもサポートしてくれます。
ライセンスはMIT Licenseとなっていますので、商用OKですね。

interact.js 公式サイト

ここで「サポート」と言っているのは、interact.js自体を読み込むだけでドラッグ&ドロップが動くようになるわけでなく、あくまでもイベントが追加されるだけだからです。 interact.jsによって追加されたイベントで簡単に座標やサイズなどが取得できるようになるので、そこから実際の位置移動やサイズ調整の処理を自身で書いていく必要があります。 まぁ公式サイトにその辺りのソースコードも掲載されているので、よほど特殊なことをしない限りはちょっとコードを眺めれば動かすところまですぐできます。

interact.jsの対応ブラウザ

公式では最新のモダンブラウザーとIE9以上となっています。
ただ経験上、IEに関しては対応してると言いつつも何かと問題がありそうな予感がするので注意ですね。

IEは早よなくなれ。。。 マジで。。。。

interact.jsの使い方

使い方を知るためにひとまず公式のデモと同じようなものを作ってみます。

図形をインタラクトに動かしてみるデモ

少し公式と変えて作ったので順番に見ていきます。

html

<body>
  <div class="info">
    <p class="info-item info-item-1">1の移動距離:<span id="js-distance-1">0</span></p>
    <p class="info-item info-item-2">2の移動距離:<span id="js-distance-2">0</span></p>
  </div>
  <!-- /.info -->
  <div class="container">
    <div class="draggable draggable-1 js-draggable">1</div>
    <!-- /.draggable -->
    <div class="draggable draggable-2 js-draggable">2</div>
    <!-- /.draggable -->
  </div>
  <!-- /.container -->
</body>

.infoはドラッグイベントで移動した距離をリアルタイムで表示させるエリアです。 .draggableが実際にドラッグする要素です。

css

案件時はSassしか使いませんがデモなので直書きです。。。
色分けして見やすいようにしただけなので、CSS自体は適当ですがとりあえず載せときます。

.container{
  overflow: hidden;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -webkit-align-items: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  width: 100%;
  min-height: 100vh;
  padding: 90px;
}

.draggable {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -webkit-align-items: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-flex-shrink: 0;
  -ms-flex-negative: 0;
  flex-shrink: 0;
  width: 100px;
  height: 100px;
  font-family: "Helvetica Neue", Helvetica, sans-serif;
  font-size: 39px;
  font-weight: 600;
  color: #fff;
  border-radius: 50%;
  touch-action: none;
  user-select: none;
}

.draggable-1{
  background: #81cf7a;
}

.draggable-2{
  background: #25bdcf;
  margin-left: 60px;
}

.info{
  position: fixed;
  top: 0;
  z-index: 100;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -webkit-align-items: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  width: 100%;
  background: #fff;
  user-select: none;
}

.info-item{
  width: 50%;
  padding: 15px;
  text-align: center;
  font-size: 12px;
}

.info-item p{
  line-height: 2;
}

.info-item-1{
  color: #81cf7a;
  border: 1px solid currentcolor;
}

.info-item-2{
  color: #25bdcf;
  border: 1px solid currentcolor;
}

JavaScript

今回はES6+で書くのでCDNでなくwebpackでバンドルして使います。

interact.jsをインストール

まずはyarnもしくはnpmでインストールします。

$ yarn add -D interactjs
$ npm install -D interactjs

コード

今回はES6+でClassを使って書いてみます。
嫌われてがちなjQueryも不要です。

import interact from 'interactjs'

class Interact {
  constructor(){
    this.applyInteractJs();
  }

  //interact.jsを適用
  applyInteractJs(){
    this.createPositionStorage();
    this.addDraggingEvent();
  }

  //位置情報を保存する変数を作成
  createPositionStorage(){
    this.target = document.querySelectorAll('.js-draggable');
    if(!this.target){
      return;
    }
    const arr = [...this.target];
    this.editStyle = {};
    arr.forEach((el) => {
      this.editStyle[`target-${el.textContent}`] = {
        x     : 0,
        y     : 0,
        scale : 1
      };
    });
  }

  //ドラッグイベントを適用
  addDraggingEvent(){
    if(!this.target){
      return;
    }

    interact('.js-draggable')
    //マルチタッチジェスチャーイベント
    .gesturable({
      onmove: (e) => {
        //2本指での拡大縮小に対応
        this.scaleMoveListener(e);
      }
    })
    //ドラッグ&ドロップイベント
    .draggable({
      // 慣性を付ける
      inertia     : true,
      // 親要素の領域内に要素を保持します
      modifiers   : [
      interact.modifiers.restrictRect({
        //ドラッグの範囲を親要素に設定
        restriction : 'parent',
        //ドラッグの最後の移動値を適用させる。慣性と併用することで動きがスムーズになる
        endOnly     : true
      })
      ],
      // コンテナの端でドラッグまたはサイズ変更の移動が発生した場合、コンテナ(ウィンドウまたはHTMLElement)をスクロールします。
      autoScroll  : true,

      // dragmoveイベント
      onmove      : (e) => {
        this.dragMoveListener(e);
      },
      // dragend イベント
      onend       : (e) => {
        this.dragEndListner(e);
      }
    });
  }

  /*
  * マルチタッチジェスチャー(2本指ピンチ)イベントで拡大縮小
  * @param {Object} event
  */
  scaleMoveListener(event){
    event.stopPropagation();
    const target    = event.target;
    const target_id = target.textContent;
    const editStyle = this.editStyle[`target-${target_id}`];

    //拡大縮小値を更新
    editStyle.scale = event.scale;

    // css transform で要素を動かす
    this.addEditedStyle(target, editStyle);

    //情報を表示させる
    this.showPositionInfomation(target_id, event);
  }

  /*
  * dragmoveイベントを取得
  * @param {Object} event
  */
  dragMoveListener(event){
    event.stopPropagation();
    const target    = event.target;
    const target_id = target.textContent;
    const editStyle  = this.editStyle[`target-${target_id}`];

    //位置情報を更新する
    editStyle.x = (parseFloat(editStyle.x) || 0) + event.dx;
    editStyle.y = (parseFloat(editStyle.y) || 0) + event.dy;

    // css transform で要素を動かす
    this.addEditedStyle(target, editStyle);

    //情報を表示させる
    this.showPositionInfomation(target_id, event);
  }

  /*
  * 操作した図形のスタイルを追加する
  * @param {Object} editStyle 図形の編集情報
  */
  addEditedStyle(target, editStyle){
     // css transform で要素を動かす
     target.style.webkitTransform =
     target.style.transform =
     `translate(${editStyle.x}px, ${editStyle.y}px) scale(${editStyle.scale}`;
   }

  /*
  * dragendイベントを取得
  * @param {Object} event
  */
  dragEndListner(event){
    const target_id = event.target.textContent;
    this.showPositionInfomation(target_id, event);
  }

  /*
  * 位置情報を表示させる
  * @param {String} target_id
  * @param {Object} event
  */
  showPositionInfomation(target_id, event){
    //移動距離
    const target = document.getElementById(`js-distance-${target_id}`);
    if(!target){
      return;
    }
    const moveValue = (Math.sqrt(Math.pow(event.pageX - event.x0, 2) + Math.pow(event.pageY - event.y0, 2) | 0)).toFixed(2) + 'px';
    target.textContent = moveValue;

    //拡大縮小値
    const targetScale = document.getElementById(`js-scale-${target_id}`);
    if(!targetScale){
      return;
    }
    targetScale.textContent = event.scale ? event.scale.toFixed(2) : 1;

  }

}
new Interact();

全体はこうなりました。
スマホでの2本指ピンチでの拡大縮小機能も追加してみました。
以下ポイントだけみていきます。

htmlのdata属性は使わない

公式のサンプルではhtmlのdata属性を使って位置情報の保存や取得を行っていますが、JSの中で完結できるのでその手法は使いません。 生DOMをいじくるのは最小限にしておきたいですね。

代わりにメンバ変数を使います。 メンバ変数はClass内に閉じ込められるので便利です。 要素の数だけx、yの位置情報を保存できるようにオブジェクトを生成します。

//位置情報を保存する変数を作成
createPositionStorage(){
  this.target = document.querySelectorAll('.js-draggable');
  if(!this.target){
    return;
  }
  const arr = [...this.target];
  this.editStyle = {};
  arr.forEach((el) => {
    this.editStyle[`target-${el.textContent}`] = {
      x     : 0,
      y     : 0,
      scale : 1
    };
  });
}

位置情報を計算してcssで動かす

最初に書きましたが、interact.jsはイベントを追加してくれるだけです。
そのため必要な値を自分で取得して位置情報を計算し、cssでターゲットとなる図形を動かします。

/*
* dragmoveイベントを取得
* @param {Object} event
*/
dragMoveListener(event){
  //移動前の位置情報を取得する
  event.stopPropagation();
  const target    = event.target;
  const target_id = target.textContent;
  const editStyle = this.editStyle[`target-${target_id}`];

  //移動後の位置情報に更新する
  editStyle.x = (parseFloat(editStyle.x) || 0) + event.dx;
  editStyle.y = (parseFloat(editStyle.y) || 0) + event.dy;

  // css transform で要素を動かす
  this.addEditedStyle(target, editStyle);

  //位置情報を表示させる
  this.showPositionInfomation(target_id, event);
}

draggableイベントのonmoveに指定したdragMoveListenerという関数で「図形をドラッグして動かしている最中」の図形の位置移動を処理します。
ここではドラッグしている要素の元の位置情報を変数(this.position)から取り出し、イベントオブジェクトから動いた位置(event.dx、event.dy)を取得して加算することで位置情報を割り出します。

割り出した位置情報をCSSのtransformで要素に適用させると位置が動きます。
最後に変数(this.position)を更新すれば、onmoveでドラッグを動かしている最中は連続的に位置が更新されて図形がドラッグされた状態になります。

元の位置情報(this.position)を取得 => 動かした後の位置を元の位置に加算(this.position.x + event.dx、this.position.y + event.dy)=> 位置情報(this.position)を更新

これがドラッグイベント発生中に連続的に実行されるって感じですね。

ちなみに公式のサンプルと変数や関数名などが少し違いますが、計算式や処理内容はほとんど全く同じです。 動かしたいだけなら公式をコピペするのが早いですが、こうやってサンプルを紐解いて仕組みを理解しないと他のものに適用できないのがinteract.jsのやっかいなところです。 特にフレームワークと併用するときなんかはdata属性なんか使わないだろうしコピペだけでは無理ですね。

位置情報を画面に表示させる

/*
* 位置情報を表示させる
* @param {String} target_id
* @param {Object} event
*/
showPositionInfomation(target_id, event){
  //移動距離
  const target = document.getElementById(`js-distance-${target_id}`);
  if(!target){
    return;
  }
  const moveValue = (Math.sqrt(Math.pow(event.pageX - event.x0, 2) + Math.pow(event.pageY - event.y0, 2) | 0)).toFixed(2) + 'px';
  target.textContent = moveValue;

  //拡大縮小値
  const targetScale = document.getElementById(`js-scale-${target_id}`);
  if(!targetScale){
    return;
  }
  targetScale.textContent = event.scale ? event.scale.toFixed(2) : 1;

}

こちらは関数化しましたが、公式のサンプルと計算式は全く同じです。
これをonmoveonend時に使ってあげると位置情報をリアルタイムで表示できます。 公式ではonend時だけですがonmoveにもいれてみました。

DEMO

図形をインタラクトに動かしてみるDEMO
図形をインタラクトに動かしてみるDEMO

PCのマウスでのドラッグ&ドロップはもちろん、スマホでの2本指ピンチでの拡大縮小にも対応しています。(ピンチ機能はPCのトラックパッドには対応していません)

さいごに

ここまででドラッグ&ドロップの簡単な使い方はわかりましたね。
他にもPCでのリサイズ機能やスナップ機能などもありますが、それはまた時間がある時に追記します!

次の記事で応用編としてinteract.jsとcanvasを使って画像を動かしてみようと思います!