15
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React-DnDでドラッグ&ドロップを行う(モバイル対応も)

Last updated at Posted at 2019-03-01

こんにちは、ミツバです。
今回はReact-DnDを利用して、ドラッグ&ドロップを用いたリストの並び替えを実現したいと思います。

react-dnd

ソースコードは以下に置いています。

mitubaEX/react-dnd-example

動作イメージ

drag

テスト環境

テストを行った際の各ライブラリのバージョンを明記しておきます。

  • react: ^15.6.2
  • react-dnd: ^2.6.0
  • react-dnd-html5-backend: ^2.6.0
  • react-dnd-touch-backend: ^0.4.0
  • react-dom: ^15.6.2
  • react-scripts: 2.1.5

setup

create-react-appを用いてサクッと作成します。

npx create-react-app react-dnd-example
cd react-dnd-example

# install
npm install --save react-dnd@2.6.0
npm install --save react-dnd-html5-backend@2.6.0
npm install --save react-dnd-touch-backend@0.4.0

package.jsonの各バージョンは適宜変更してください。

App.jsを変更する

import React, { Component } from 'react';
import SortableList from './SortableList.js';

class App extends Component {
  render() {
    return (
      <div className="App">
        <SortableList />
      </div>
    );
  }
}

export default App;

SortableListを作成する

import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import DragItem from './DragItem';
import CustomDragLayer from './CustomDragLayer';

class SortableList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [
        {id: 1, name: "aaa"},
        {id: 2, name: "bbb"},
        {id: 3, name: "ccc"}
      ],
    }
  }

  onDrop = (toId, fromId) => {
    const items = this.state.items.slice();
    const toIndex = items.findIndex(i => i.id === toId);
    const fromIndex = items.findIndex(i => i.id === fromId);
    const toItem = items[toIndex];
    const fromItem = items[fromIndex];
    items[toIndex] = fromItem;
    items[fromIndex] = toItem;
    this.setState({items})
  }

  render () {
    return (
      <div>
        {(isAndroid() || isIOS()) && <CustomDragLayer/>}
        {
          this.state.items.map(item => {
            return (
              <DragItem
                key={item.id}
                id={item.id}
                onDrop={this.onDrop.bind(this)}
                name={item.name}
              />
            )
        })
  }
</div>
    )
}
}

function isAndroid() {
  return !!window.navigator.userAgent.match(/Android/);
}

function isIOS() {
  return !!window.navigator.userAgent.match(/iPhone|iPad|iPod/);
}

export default DragDropContext((isAndroid() || isIOS()) ? TouchBackend : HTML5Backend)(SortableList);

いろいろ概念が登場していますが、CustomDragLayerDragItemをrenderしています。
そして最後にDragDropContextに分岐を書いてモバイルの場合とそうでない場合のBackendを切り替えています。
このBackendは、今回HTML5とTouchの2種類ありデバイスによって、Backendを変更することでそのデバイスに対応します。

DragItem, CustomDragLayerを作成する

DragItemは以下のような感じです。

import React, { Component } from 'react';
import { DragSource, DropTarget } from 'react-dnd'

// ドラッグされるSourceの動作を定義する
const dragSource = DragSource("item", {
  beginDrag(props) {
    return props;
  }
}, (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
})

// ドロップされるTargetの動作を定義する
const dropTarget = DropTarget("item", {
  drop(dropProps, monitor) {
    const dragProps = monitor.getItem(); 
    if (dropProps.id !== dragProps.id) {
      dragProps.onDrop(dragProps.id, dropProps.id);
    }
  }
}, connect => {
  return {
    connectDropTarget: connect.dropTarget()
  };
})

class DragItem extends Component {
  constructor(props) {
    super(props)
  }

  getItemStyles() {
    const { isDragging } = this.props

    return {
      opacity: isDragging ? 0.4 : 1,
    }
  }

  render() {
    return this.props.connectDragSource(
      this.props.connectDropTarget(
        <div style={this.getItemStyles()}>
          {this.props.name}
        </div>
      )
    )
  }
}

export default dragSource(dropTarget(DragItem));

ドラッグされるSourceとドロップされるTargetの動作を定義し、最後にComponentを包んでやる流れです。
今回はドラッグされるものもドロップされるものも同じなので、同じComponent(DragItem)を包んでいます。

これでだいたいオッケーなのですが、モバイルだとドラッグした時の軌跡が表示されません。
そのためにDragLayerを導入し、ドラッグの軌跡をそいつに描画してもらいます。

動作イメージ

no_layer

DragLayerを利用して、包んだComponentであるCustomDragLayerは以下のような感じ。

import * as React from 'react'
import { DragLayer } from 'react-dnd'

const layerStyles = {
  position: 'fixed',
  pointerEvents: 'none',
  top: 0,
  left: 0,
  width: '100%',
  height: '100%',
}

function collect(monitor) {
  return {
    item: monitor.getItem(),
    itemType: monitor.getItemType(),
    initialOffset: monitor.getInitialSourceClientOffset(),
    currentOffset: monitor.getSourceClientOffset(),
    isDragging: monitor.isDragging()
  }
}

class CustomDragLayer extends React.Component {
  getItemStyles(currentOffset) {
    if (!currentOffset) {
      return {
        display: 'none'
      }
    }

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

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

  render() {
    const { item, itemType, isDragging, currentOffset } = this.props

    if (!isDragging) {
      return null
    }

    // render
    if (itemType === 'item') {
      return (
        <div style={layerStyles}>
          <div style={this.getItemStyles(currentOffset)}>
            {item.name}
          </div>
        </div>
      )
    }
    return null;
  }
}

export default DragLayer(collect)(CustomDragLayer)

これでモバイルでもドラッグの軌跡が表示されたと思います。

細かい対応

DragLayerをwebの方にも適応する

今回、DragLayerをモバイルのドラッグ軌跡の描画に用いましたが、ドラッグしたときのアニメーションを個別で決めたいときにも利用できます。
しかし、デフォルトだとwebのドラッグの軌跡は表示されているので、軌跡が二重に表示されます。
なので、それを防ぐために以下のissueのようにドラッグされているときは、軌跡を描画しないようにします。

connectDragPreview() component is also displayed as regular component

componentDidMount() {
  // Preview表示する時のみ、元のComponentの描画を消す
  if (this.props.connectDragPreview) {
    this.props.connectDragPreview(getEmptyImage(), {
      captureDraggingState: true,
    });
  }
}

hoverを利用して入れ替え処理を行う

DropでItemを変えるよりもより直感的に入れ替える方法として、hoverというものがあります。
それを紹介します。

DragItemを以下のように変更します。
今回はdropをhoverに変更し、そこに処理を書いていきました。
他にもいろいろAPIが提供されているので調べてみてください。


import React, { Component } from 'react';
import {
  DragSource,
  DropTarget,
} from 'react-dnd'

const dragSource = DragSource("item", {
  // 変更部分
  beginDrag(props) {
    return {
      id: props.id,
      name: props.name,
      originalIndex: props.findItem(props.id)
    };
  },

  endDrag(props: CardProps, monitor: DragSourceMonitor) {
    const { id, originalIndex } = monitor.getItem()
    const didDrop = monitor.didDrop()

    if (!didDrop) {
      props.onDrop(id, originalIndex)
    }
  },
}, (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
})

const dropTarget = DropTarget("item", {
  // 変更部分
  canDrop() {
    return false
  },

  hover(props: CardProps, monitor: DropTargetMonitor) {
    const draggedId = monitor.getItem().id
    const overId = props.id

    if (draggedId !== overId) {
      const overIndex = props.findItem(overId)
      props.onDrop(draggedId, overIndex)
    }
  },

}, connect => {
  return {
    connectDropTarget: connect.dropTarget()
  };
})

class DragItem extends Component {
  constructor(props) {
    super(props)
  }

  getItemStyles() {
    const { isDragging } = this.props

    return {
      opacity: isDragging ? 0 : 1,
    }
  }

  render() {
    return this.props.connectDragSource(
      this.props.connectDropTarget(
        <div style={this.getItemStyles()}>
          {this.props.name}
        </div>
      )
    )
  }
}

export default dragSource(dropTarget(DragItem));

そしてfindItemは以下のような定義です。
これをDragItemのpropsとして渡します。


findItem = (index) => {
  return this.state.items.filter(c => c.id === index)[0].id
}

動作イメージ

hover

まとめ

react-dndについて調べることがあったので、少し記事にしてみました。
コードペタペタ貼っただけなので、各自雰囲気で理解してください。
ドラッグ・アンド・ドロップ周りでこれがいいよとか他にあったら教えてください。

参考

react-dnd これだけ抑えておけばおk
React DnDでスマホでもドラッグアンドドロップ

15
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?