React DnDでスマホでもドラッグアンドドロップ

[Enigmo Advent Calendar 2018](https://qiita.com/advent-calendar/2018/enigmo)の12日目の記事です。

**注意: この記事のサンプルコードで使われている各ライブラリのバージョンは下記になります。**

```
react 16.4.0
react-dnd 4.0.2
react-dnd-html5-backend 4.0.2
react-dnd-touch-backend 0.5.1
```

# React DnD
Reactでドラッグアンドドロップでの並び替えを実装する際によく使われるのが[React DnD](https://github.com/react-dnd/react-dnd)というライブラリです。
このライブラリではHTML5の[Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)を利用してドラッグアンドドロップを実現していますが、このAPI自体がスマートフォンなどのタッチデバイスには対応しておらず、スマホでそのままドラッグアンドドロップを実装することができません。

# TouchBackend
React DnDを使う際、ドラッグアンドドロップしたいコンポーネントを `DragDropContext` という `HOC(Higer Order Component`) に渡します。
この `DragDropContext` の最初の引数に渡すのは通常、 [HTML5Backend](https://github.com/react-dnd/react-dnd/tree/master/packages/react-dnd-html5-backend) というバックエンドモジュールです。

```jsx
import HTML5Backend from 'react-dnd-html5-backend'
import { DragDropContext } from 'react-dnd'

class YourApp {
	/* ... */
}

export default DragDropContext(HTML5Backend)(YourApp)
```

前述した通りタッチデバイスの場合はこの `HTML5Backend` は使えません。 しかしタッチデバイス対応した [TouchBackend](https://github.com/yahoo/react-dnd-touch-backend)というものがあるのでそちらを使います。

```jsx
import HTML5Backend from 'react-dnd-html5-backend'
import TouchBackend from 'react-dnd-touch-backend';
import { DragDropContext } from 'react-dnd'

const isTouchDevice = () => {
 /* タッチデバイス判定 */
}

class YourApp {
	/* ... */
}
export default DragDropContext(isTouchDevice() ? TouchBackend : HTML5Backend)(YourApp)
```

これだけでタッチデバイス対応ができました。
しかし、 `HTML5Backend` のようにいい感じにプレビューされません。


![pc-preview.gif](https://qiita-image-store.s3.amazonaws.com/0/45330/71ac0d1e-45e1-5f1e-8e2f-0ffb07c845c6.gif)
**HTML5Backendではちゃんとプレビューされている**

---

![sp-nopreview.gif](https://qiita-image-store.s3.amazonaws.com/0/45330/a6e2e057-01a1-da60-5b4d-64bb61f78cce.gif)
**TouchBackendではプレビューされていない！**
※ ChromeのDevToolsでスマートフォンをエミュレートして録画しているためマウスカーソルが表示されています。

# DragLayer
React DnD には[DragLayer](http://react-dnd.github.io/react-dnd/docs/api/drag-layer)という、ドラッグ時のプレビュー表示をカスタマイズできるAPIがあります。
これを使うことでタッチデバイスでもいい感じのプレビューを表示することができます。

利用側のサンプルコードは以下です。

```jsx
import React from 'react'
import DragLayer from 'react-dnd/lib/DragLayer'
import TouchBackend from 'react-dnd-touch-backend';
import { DragDropContext } from 'react-dnd'

function collect(monitor) {
  const item = monitor.getItem()
  return {
    currentOffset: monitor.getSourceClientOffset(),
    previewProps: item && item.previewProps,
    isDragging:
      monitor.isDragging() && monitor.getItemType() === 'IMAGE'
  }
}

function getItemStyles(currentOffset) {
  if (!currentOffset) {
    return {
      display: 'none'
    }
  }

  const x = currentOffset.x
  const y = currentOffset.y
  const transform = `translate(${x}px, ${y}px) scale(1.05)`

  return {
    WebkitTransform: transform,
    transform: transform,
  }
}

class PreviewComponent extends React.Component {
  render() {
    const { isDragging, previewProps, currentOffset } = this.props
    if (!isDragging) {
      return null
    }

    return (
      <div style={getItemStyles(currentOffset)}>
        {/*...*/}
      </div>
    )
  }
}

const DragPreview = DragLayer(collect)(PreviewComponent)


class YourApp {
  render() {
    return (
      <div>
        {/* ... */}
        <DragPreview />
      </div>
    )
  }
}

export default DragDropContext(TouchBackend)(YourApp)
```

## かんたんに解説
`DragLayer` の引数 `collect` 関数では[DragLayerMonitor](http://react-dnd.github.io/react-dnd/docs/api/drag-layer-monitor)のオブジェクトが渡されます。
`monitor.getItem()` で `DragSource` にアクセスすることができ、 任意で渡した `props`(今回の場合は `previewProps` という名前で渡していますが、どんな名前でも渡すことができます) にアクセスできます。
また、 `monitor.isDragging` で実際にドラッグされているか判定することができます。
同一画面の他のコンポーネントでもドラッグアンドドロップするために、 `DragDropContext` が複数ある場合は `monitor.getItemType()` でどのコンテキストなのかを判定するとよいでしょう。
プレビューがタッチした部分に追従するように `monitor.getSourceClientOffset()` を使ってオフセット座標を返しておきます。
`collect` 関数の返り値のオブジェクトはそのままプレビュー用のコンポーネントで `props` として受け取ることができます。
`getItemStyles` 関数では受け取った `props.currentOffset` を使ってCSSを調整しています。


`DragDropContext` に渡したコンポーネントで `DragLayer` を描画することで、ドラッグ時にプレビューを表示することができます。

---

![sp-preview.gif](https://qiita-image-store.s3.amazonaws.com/0/45330/5c597f4d-13e4-bf2d-f9d6-166f8302c7d0.gif)
**スマホでもプレビューができた!**
※ ChromeのDevToolsでスマートフォンをエミュレートして録画しているためマウスカーソルが表示されています。

# 最後に
スマートフォンなどのタッチデバイスでHTML5のようなドラッグアンドドロップを実現する方法を解説しました。
実際に実装する際は、[TouchBackendのリポジトリ](https://github.com/yahoo/react-dnd-touch-backend/tree/master/examples) に完全に動作するサンプルがあるのでそちらも参考にしてみてください。

# 参考リンク
http://react-dnd.github.io/react-dnd/about
https://github.com/yahoo/react-dnd-touch-backend