Javascriptでエレメントをフリードラッギング

# 概要

Javascriptでドラッグ&ドロップするライブラリは山ほどありますが、画面にフローティングウィンドウのようなエレメントを配置して自由にドラッグできるようにしたかっただけなので、簡単に自分で実装できないかと思って試したら意外に面倒だったという話です。

# コード

```js
export function draggable(element) {
  element.style.position = 'absolute'
  // ドラッグ中のtransformの値を保持
  const transformPos = {x: 0, y: 0}
  // ドラッグスタート時のマウスとエレメントのオフセット
  const dragStartOffset = {x: 0, y: 0}


  // ハンドルにイベント登録
  const handles = element.querySelectorAll('.draggable-handle')
  for (var i = 0; i < handles.length; i++) {
    handles[i].addEventListener('mousedown', startDragging)
    handles[i].addEventListener('touchstart', startDragging)
  }

  function getClientPosition(event){
    if(event.touches){
      return {x: event.touches[0].clientX, y: event.touches[0].clientY}
    } else {
      return {x: event.clientX, y: event.clientY}
    }
  }

  function startDragging(e) {
    e.preventDefault()

    const clientPos = getClientPosition(e)
    dragStartOffset.x = clientPos.x - element.offsetLeft
    dragStartOffset.y = clientPos.y - element.offsetTop

    document.addEventListener('mouseup', endDragging)
    document.addEventListener('touchend', endDragging)
    document.addEventListener('mousemove', elementDrag)
    // [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive.
    // See https://www.chromestatus.com/features/5093566007214080
    document.addEventListener('touchmove', elementDrag, { passive: false })
  }

  function elementDrag(e) {
    e.preventDefault()

    const clientPos = getClientPosition(e)
    transformPos.x = clientPos.x - element.offsetLeft - dragStartOffset.x
    transformPos.y = clientPos.y - element.offsetTop - dragStartOffset.y
    element.style.transform = `translate(${transformPos.x}px,${transformPos.y}px)`
  }

  function endDragging() {
    document.removeEventListener('mouseup', endDragging)
    document.removeEventListener('touchend', endDragging)
    document.removeEventListener('mousemove', elementDrag)
    document.removeEventListener('touchmove', elementDrag)

    // ドラッグ終了時に場所とtransformをリセット
    element.style.top = (element.offsetTop + transformPos.y) + "px";
    element.style.left = (element.offsetLeft + transformPos.x) + "px";
    transformPos.x = 0
    transformPos.y = 0
    element.style.transform = `translate(${transformPos.x}px,${transformPos.y}px)`

    // fire event
    const event = new CustomEvent('draggable.drop')
    element.dispatchEvent(event)
  }
}
```

```js
import { draggable } from '../functions/draggable'

const elem = document.querySelector('#dialog')
draggable(elem)
```

```html
  <div id="dialog">
    <div class="draggable-handle" style="top: 100px; left: 200px;">ダイアログ</div>
    <div>...</div>
  </div>
```

# 解説

[こちら](https://www.w3schools.com/howto/howto_js_draggable.asp)を参考にしましたがいくつか変更してます。

* スマホでも動くようにtouchに対応。
* ドラッグ中はCSSのtransformで場所を変更するようにした。
* ストップ時にイベント発火。

戦略として面白いと思ったのはドラッグ中のみイベントを登録し、止まるとイベントを削除してしまうところですかね。コードはできるだけ最低限に留めておきましたが、absoluteをここでやるのは微妙かもしれないですね。繰り返し登録しないようにするとか工夫するのもありかも。

Chrome以外テストしてません。`new CustomEvent`がたしかIEダメでしたよね。まあ何か気づいたら修正しておきます。


# reactで使う

おまけ。Reactのアプリ内で使ったので参考までに置いておきます。

```react
import classNames from 'classnames'
import React, { Component } from 'react'
import { draggable } from '../functions/draggable'

export class DraggableHandle extends Component {
  render () {
    return (
      <div className={classNames('draggable-handle', this.props.className)}>
        {this.props.children}
      </div>
    )
  }
}

export default class Draggable extends Component {
  constructor(props){
    super(props)
    this.state = {
      left: this.props.left || 0,
      top: this.props.top || 0,
    }

    this.elemRef = React.createRef()
  }

  componentDidMount(){
    draggable(this.elemRef.current)
    this.elemRef.current.addEventListener('draggable.drop', e => this.props.onDrop(e))
  }

  componentDidUpdate(prevProps) {
    if (this.props.left !== prevProps.left || this.props.top !== prevProps.top) {
      this.setState({top: this.props.top, left: this.props.left})
    }
  }

  render () {
    return (
      <div
        ref={this.elemRef}
        style={{top: this.state.top, left: this.state.left}}
        className={this.props.className}
      >
        {this.props.children}
      </div>
    )
  }
}


Draggable.defaultProps = {
  onDrop: event => {}
}
```

```react
import Draggable, { DraggableHandle } from '../../../shares/components/Draggable'

class App extends Component {
  render(){
    return(
      <Draggable top={0} left={0} onDrop={data => console.log(data)}>
        <DraggableHandle>ダイアログ</DraggableHandle>
        <div>...</div>
      </Draggable>
    )
  }
}
```