jQueryUIのdraggableウィジェットとHTML5 ドラッグアンドドロップAPIを併用する方法

# 結論

子要素の`mousedown`イベントのバブリングを止めると併用できます。
https://codepen.io/ledsun/pen/qBdOaMB にサンプルがあります。

# 背景

ブラウザに表示されているDOM要素をマウス操作で移動したいことがあります。

現在ではHTML5対応ブラウザが用意しているドラッグアンドドロップ用のAPIが使えます[^1]。2008年から2010年にかけて、各種ブラウザはドラッグアンドドロップAPIをサポートしました[^3]。

[^3]: https://caniuse.com/#feat=dragndrop
[^1]: https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API

ブラウザがドラッグアンドドロップをサポートする前も、JavaScriptを使ってマウス操作でのDOM要素移動を実現していたライブラリがありました。そのひとつにjQueryUIの`draggable`ウィジェット[^4]があります。jQueryUIは2008年頃から2012年頃に最も盛んに開発されていました。[^2]

[^4]: https://jqueryui.com/draggable/
[^2]: https://jqueryui.com/changelog/


この2つの「マウス操作でのDOM要素移動方法」の併用は考慮されていません。jQueryUIの`draggable`ウィジェットを適用したDOM要素の子要素に、HTML5 ドラッグアンドドロップAPIを、素直に使うと、動きません。

<img width="527" alt="スクリーンショット 2020-05-14 23.01.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15964/426cfe90-c77a-8f1b-6f0c-0933e4c9faf7.png">


逆に、HTML5 ドラッグアンドドロップAPIを適用したDOM要素の子要素に、jQueryUIの`draggable`ウィジェットを適用しても、特に問題は起きません。

# 単体での使い方

次にそれぞれの使用方法を見ていきましょう。

## jQueryUIのdraggableウィジェット

実に簡単に使えます。次のようにjQueryオブジェクトのdraggable関数を呼びます。

```js
$('div').draggable()
```

対象DOM要素の上でマウスボタンを押下してから、マウスを移動すると、対象DOM要素が移動します。
https://jqueryui.com/draggable/ のデモがわかりやすいです。

## HTML5 ドラッグアンドドロップAPI

こちらも簡単です。draggable属性をtrueにします。

```html
<span draggable="true">ABC</span>
```

jQueryUIのdraggable関数と、ドラッグしたときの動きが少し違います。ドラッグするとゴースト画像が移動し、要素は移動しません。

要素の移動処理は、さらにdropイベントのイベントリスナーを実装します。
https://www.html5rocks.com/ja/tutorials/dnd/basics/ の説明とデモがわかりやすいです。

jQueryUIの使い始めやすさと比べると少し手間です。
この違いは、HTML5のドラッグアンドドロップAPIは、ブラウザのAPIです。このAPIを使って様々なドラッグアンドドロップを実現できるようになっています。例えば、ドロップできない要素上でマウスボタンを離すと、一度開始した移動操作をキャンセルできます。

また、ゴースト画像をブラウザが自動的に生成してくれるのは、大変便利です。ゴースト画像はドラッグアンドドロップの操作としては、直感的ですが、JavaScriptで動的に作ろうとすると大変です。

# 両者を組み合わせて使う

次のようにdivと、その子要素のspanがあります。子要素のspanに、HTML5 ドラッグアンドドロップAPIを適用します。


```html
<div>
  <span id="abc" draggable="true">ABC</span>
</div>
```

親要素のdivにjQueryUIの`draggable`ウィジェットを適用します。

```js
$('div').draggable()
```

spanを移動しようとしても、HTML5のドラッグアンドドロップは開始しせず、ゴースト画像は表示されません。親要素のdivがjQueryUIの`draggable`のドラッグ動作をします。

なぜでしょうか？

# 原因

## dragstartイベントが発火しない

HTML5のドラッグアンドドロップの開始イベント`dragstart`が発火していません。
次のようにspan要素に`dragstart`イベントのイベントリスナーを登録します。

```js
document.querySelector('#abc').addEventListener('dragstart', () => {
  console.log('hi')
})
```

spanを、マウス操作で移動しようとしても、開発コンソールには、何も出力されません。このイベントリスナーは実行されません。

## jQueryUIの`draggable`ウィジェットは`mousedown`イベントのデフォルト処理を止める

jQueryUIにはマウスイベントを管理するための、`ui.mouse`ウィジェットがあります。このウィジェットの`_mouseDown`関数は、mousedownイベントに対して`preventDefault`関数を呼んで、デフォルト処理を止めています[^6]。

[^6]: https://github.com/jquery/jquery-ui/blob/1.12.1/ui/widgets/mouse.js#L130

`draggable`ウィジェットは`ui.mouse`ウィジェットを使っています。`draggable`ウィジェットを適用したdivへの`mousedown`イベントのデフォルト処理を止めます。

なぜ`mousedown`イベントのデフォルト処理を止めると、`dragstart`イベントが発火しなくなるのでしょうか？
`mousedown`イベントと`dragstart`イベントにはどのような関係があるのでしょうか？


## dragstartイベントはmousedownイベントとそれに続くmousemoveイベント

WHATWGのHTML仕様のドラッグアンドドロップの章[^5]に次のような記述があります。

[^5]: https://html.spec.whatwg.org/multipage/dnd.html#dnd

> a drag operation could be the default action of a mousedown event that is followed by a series of mousemove events

直訳すれば

> ドラッグ操作は、一連のマウス移動イベントの後に続くマウスダウンイベントのデフォルトアクションになります。

`dragstart`が発火するためには、まず`mousedown`イベントが発火する必要がありそうです。

次のような独立した、HTML5 ドラッグアンドドロップAPIを適用したspanがあります。

```html
<span id="abc" draggable="true">ABC</span>
```

このときHTML5 ドラッグアンドドロップは動きます。

次のようなイベントリスナーを登録して、`mousedown`イベントのデフォルト処理を止めます。

```js
document.querySelector('#abc').addEventListener('mousedown', (e) => {
  e.preventDefault()
})
```

ドラッグアンドドロップは動かなくなります。`dragstart`が発火するためには、まず`mousedown`イベントが発火し、さらにデフォルト動作が実行される必要があります。

jQueryUIの`draggable`ウィジェットは、親要素の`mousedown`イベントのデフォルト処理を止めているだけです。子要素の`mousedown`イベントまで、デフォルト処理が止まるのはなぜでしょうか？

## イベントバブリング

[バブリング と キャプチャリング](https://ja.javascript.info/bubbling-and-capturing)に説明があるように、あるDOM要素に発火したイベントは、次に親要素のイベントリスナーを呼び出します。

子要素で発火した`mousedown`イベントは、親要素のイベントリスナーを呼び出します。親要素のイベントリスナーは、子要素のイベントのデフォルト動作を止められます。

次のHTMLは、子要素がHTML5 ドラッグアンドドロップできます。

```html
<div>
  <span id="abc" draggable="true">ABC</span>
</div>
```

親要素のイベントリスナーで`mousedown`イベントのデフォルト動作を止めます。

```js
document.querySelector('div').addEventListener('mousedown', (e) => {
  e.preventDefault()
})
```

ドラッグアンドドロップは動かなくなります。


## イベントリスナーの実行順序

DOM要素をマウス操作で移動しようとしたとき、

1. 子要素の`dragstart`のイベントリスナー
2. 親要素の`mousedown`のイベントリスナー

はどちらが先に実行されるでしょうか？子要素のイベントリスナーが先に実行されることはないのでしょうか？

`dragstart`が発火するには、`mousedown`イベントが発火したあとに、`mousemove`イベントが発火する必要があります。ブラウザの実装は確認していませんが、大抵のブラウザでは、「親要素の`mousedown`のイベントリスナー」が先に実行されそうです。


## 結論

親要素の`mousedown`イベントのイベントリスナーがデフォルト動作を止めているため、`dragstart`イベントが発火していません。


# 対策

## 子要素の`mousedown`イベントのバブリングを止める

イベントバブリングはstopPropagation関数をつかえば止められます。

```html
<div>
  <span id="abc" draggable="true">ABC</span>
</div>
```

次ように子要素の`mousedown`イベントリスナーでイベントのバブリングを止めます。

```js
$('div').draggable()
document.querySelector('#abc').addEventListener('mousedown', (e) => {
  e.stopPropagation()
})
```

親要素のjQueryUIの`darggable`ウィジェットを使ったマウス操作での移動と、子要素のHTML5 ドラッグアンドドロップAPIを使った移動が併用できます。

これで既にjQueryUIの`darggable`ウィジェットを適用した要素のなかに、新しくドラッグアンドドロップ操作を追加しなければいけなくなっても安心です。

# まとめ

今回のエスパーポイントは、

1. `dragstart`イベントが発火しない現象
2. `mousedown`イベントのデフォルト動作を止めているロジック

両者の、関係を感知するところです。

なぜこのようなエスパーができるのでしょうか？「ドラッグスタートイベントはマウスダウンイベントとマウスムーブイベントの組み合わせである」と知っていたからです。

[Reactive Extensionsを使ったWPFのドラッグアンドドロップをまとめる - Qiita](https://qiita.com/ledsun/items/1ab669ecbea103972e7d)
で書いたように、過去にマウスダウンイベントとマウスムーブイベントを組み合わせてドラッグアンドドロップを実装した経験があります。この経験が生きました。WPFとブラウザは実行環境は違いますが、ドラッグアンドドロップの概念は一緒でした。

# その他の過去のドラッグアンドドロップに関する記事

- [Reactive Extensionsを使ったドラッグアンドドロップで途中状態を表示する - Qiita](https://qiita.com/ledsun/items/b98298b131db23b04b16)
- [RedmineプラグインでWikiのサイドバーにページツリービューを表示する - Qiita](https://qiita.com/ledsun/items/c73a4b5ad3fa4e4e992d)