概要
Javascriptでドラッグ&ドロップするライブラリは山ほどありますが、画面にフローティングウィンドウのようなエレメントを配置して自由にドラッグできるようにしたかっただけなので、簡単に自分で実装できないかと思って試したら意外に面倒だったという話です。
コード
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)
}
}
import { draggable } from '../functions/draggable'
const elem = document.querySelector('#dialog')
draggable(elem)
<div id="dialog">
<div class="draggable-handle" style="top: 100px; left: 200px;">ダイアログ</div>
<div>...</div>
</div>
解説
こちらを参考にしましたがいくつか変更してます。
- スマホでも動くようにtouchに対応。
- ドラッグ中はCSSのtransformで場所を変更するようにした。
- ストップ時にイベント発火。
戦略として面白いと思ったのはドラッグ中のみイベントを登録し、止まるとイベントを削除してしまうところですかね。コードはできるだけ最低限に留めておきましたが、absoluteをここでやるのは微妙かもしれないですね。繰り返し登録しないようにするとか工夫するのもありかも。
Chrome以外テストしてません。new CustomEvent
がたしかIEダメでしたよね。まあ何か気づいたら修正しておきます。
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 => {}
}
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>
)
}
}