ng2-dragulaでコピーと入れ子のドラッグアンドドロップを実現する方法

[以前](http://qiita.com/frost_star/items/54f0c954766dbf4d95b2)、ng2-dndというライブラリを使ってAngular2のドラッグアンドドロップを実現する方法について書きました。
ng2-dndはHTML5準拠のdnd処理で非常にいい感じなのですが、入れ子のソートやオブジェクトをコピーするような処理を実現できません。
そこで、ng2-dragulaという別のライブラリを使ってコピーしたパーツを入れ子で配置するという処理を実装してみました。
このような処理が使われる例としては、リソースエディタみたいにツールバーからアイテムを引っ張ってきて配置するようなものが考えられます。

# ng2-dragulaとは？
[ng2-dragula](http://valor-software.com/ng2-dragula/index.html)はng2と付くくらいなのでAngular2のライブラリです。
ただ、本体は[dragula](http://bevacqua.github.io/angular-dragula/)というJavaScriptライブラリであり、ng2-dragula自体はAngular2用のラッパーです。
Angular2以外にもAngularやReact用のラッパーも公開されています。
今回はAngular2で実装したのでng2-dragulaで書いてますが、読み替えてもらえば他のやつでも動くのではないかと思います。

# ng2-dragulaの導入
とりあえず、[公式の手順](https://github.com/valor-software/ng2-dragula)に従いng2-dragulaを導入していきます。
`npm install ng2-dragula dragula --save`
でdragula本体とng2-dragulaをnpmからインストールします。
あとは、NgModuleの中のimportsにDragulaModuleを入れるだけ。

```ts:app.ts

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    DragulaModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})

export class AppModule {
}
```

ただし、このままだとドラッグ中の表示がおかしいのでdragula(.min).cssを読み込むことで、ドラッグ時に半透明のオブジェクトが追従するようなエフェクトがで動くようになります。

# コピー&入れ子の処理をつくる

## 単純なDnD処理の作成
まずは、コピーも入れ子もない単純なDnD処理を作成します。
つくるtsファイルは以下の4種類。
設定ファイル等は除きます。

* app.ts
* appMain.ts
* sidebar.ts
* blank.ts

### app.ts
app.tsはエントリポイントとなる部分です。
基本的にはng2-dragulaの導入の部分で書いたやつと同じです。
他のコンポーネントの読み込み処理とかAppComponentの定義を追加するくらい。

```ts:app.ts
@Component({
  selector: "app-body",
  template: `
    <app-main></app-main>
  `
})
class AppComponent {
}
```

### appMain.ts
appMain.tsはドラッグ対象エリアなどを定義する部分です。
また、ここのtemplateでsidebarコンポーネントを読み込みます。

```ts:appMain.ts
@Component({
  selector: "app-main",
  styles: [`
      /*省略*/
  `],
  template: `
  <main>
    <sidebar></sidebar>
    <div class="body">
      <div class="drag-area" [dragula]='"tool-bag"'>
      </div>
    </div>
  </main>
  `
})
export class AppMainComponent {
}
```
drag-areaに`[dragula]='"tool-bag"'`を設定し、同じ識別子を持つdragulaオブジェクトをドラッグ可能にします。
間違いやすいですが、dragulaのオブジェクトの識別子は`'"識別子"'`というようにダブルクォーテーションで囲ったものをさらにシングルクォーテーションで囲う必要があります。

### sidebar.ts
sidebar.tsはサイドバーの表示とそこからのDnD処理を定義します。

```ts:sidebar.ts
@Component({
  selector: "sidebar",
  styles: [`
      /*省略*/
  `],
  template: `
    <div class="row">
      <div class="sidebar">
        <div id="toolList" class="tools" [dragula]='"tool-bag"'>
          <div>
            <blank></blank>
          </div>
        </div>
      </div>
    </div>
  `
})
export class SidebarComponent {
}
```

` [dragula]='"tool-bag"'`をblankの外側に定義し、blank全体をドラッグ対象オブジェクトとして扱います。

### blank.ts
blank.tsはドラッグするオブジェクトの中身です。

```ts:blank.ts
@Component({
  selector: "blank",
  styles: [`
      /*省略*/
  `],
  template: `
    <div class="tool">
      <div class="header">Block
      </div>
      <div class="block">
        <div>
        </div>
        <div class="blank">
            Place to drop
        </div>
      </div>
    </div>
  `
})
export class BlankComponent {
}
```

まだ単純なDnD処理なので表示処理くらいです。

ここまでできると、DnD処理として動かすことができます。
左側にあるBlockのPlace to dropが見えなくなっているのはcssでdisplay:noneにしているためです。
![image](https://qiita-image-store.s3.amazonaws.com/0/115996/50fbc296-3615-002a-d9f0-4f17d9e87191.png)

左側のBlockを動かすと右側に展開されますが、左側からは消えるので1個までしかオブジェクトを配置することができません。
![image](https://qiita-image-store.s3.amazonaws.com/0/115996/cb907238-e35e-5144-085d-be8c6ede884b.png)


## オブジェクトのコピー
ドラッグ元のオブジェクトをコピーする機能はdragulaのオプションとして用意されています。
コピーしたいオブジェクトがあるComponentのコンストラクタでDragulaServiceのインスタンスを受け取り、それを使って設定します。
今回の場合、SidebarComponentで設定します。

```ts:sidebar.ts
//DragulaServiceをインポート
import { DragulaService } from "ng2-dragula/ng2-dragula";

@Component({
  /*同じなので省略*/
})
export class SidebarComponent {
  constructor(private dragulaService: DragulaService) {
    dragulaService.setOptions("tool-bag", {
      copy: function (el: any , source: any) {
        return source.id === "toolList";
      },
      /*左側に置けなくするため*/
      accepts: function(el: any , source: any) {
        return source.id !== "toolList";
      }
    });
  }
}
```

copyがtrueなら、そのオブジェクトをドラッグ時にコピーするようになります。
また、acceptsも設定し、左のサイドバー内にはドロップできないようにしています。

![image](https://qiita-image-store.s3.amazonaws.com/0/115996/7ea7710a-ecd6-2d1e-3d9b-d971266796c2.png)

こんな感じで、元のオブジェクトをドラッグしても消えなくなり、複数のオブジェクトを置くことができます。
ですが、まだ入れ子の設定をしていないので、右側に置いたBlockの中にさらにBlockを配置するといったことはできません。

## 入れ子を試してみる
入れ子にする処理自体は割と簡単で、Place to dropとなっている部分にdragulaを設定すればいいだけです。

```ts:blank.ts
<div class="blank" [dragula]='"tool-bag"'>
    Place to drop
</div>
```

これでできるはず…なんですが、このままだと入れ子が動かないです。
原因はコピー処理にあります。
dragulaでは初回実行時(厳密なタイミングは不明ですが)に各オブジェクトがDragulaに登録されます。
一方、コピー処理ではドラッグするオブジェクトのコピーを生成します。
そのため、コピーされたオブジェクトはまだ登録されていないことになります。
そのため、コピーしたタイミングで手動で登録してやる必要があります。

### dragulaへのオブジェクトの登録
コピーしたタイミングでオブジェクトの登録をしたいので、コピー処理のコールバックを設定します。
clonedというイベントが存在するので、SidebarComponentでコールバックを設定し、その中でdragulaへのオブジェクトの登録を行います。

```ts:sidebar.ts
export class SidebarComponent {
  constructor(private dragulaService: DragulaService) {
    dragulaService.setOptions("tool-bag", {
      /*省略*/
    });
    //コピー処理が実行された時のイベント
    dragulaService.cloned.subscribe((value: any) => {
      if (value[3] === "copy") {
        // コピーしたオブジェクトに対してDragulaを有効化する
        //登録するHTMLElementの取得
        let blank = value[1].getElementsByClassName("blank")[0];
        //dragulaへの登録(DnDの有効化)
        this.dragulaService.find("tool-bag").drake.containers.push(blank);
      }
    });
  }
}
```

これによって、コピーと入れ子の処理の共存が実現できます。

# 動かしてみる
![image](https://qiita-image-store.s3.amazonaws.com/0/115996/7f51b6d3-6d4b-37ef-81c1-2deea1c91656.png)

これでコピーと入れ子のドラッグアンドドロップ処理ができるようになりました。
今回のソースコードは[ここ](https://github.com/frost-star/dragula_nest_copy)に置いておきます。

# 2016/12/18追記
Model(dragulaModel)で扱う場合は逆にclonedでDragulaのイベント有効化処理を行わないほうがいいようです。
詳細はそのうちまとめようと思います。