結論
子要素のmousedown
イベントのバブリングを止めると併用できます。
https://codepen.io/ledsun/pen/qBdOaMB にサンプルがあります。
背景
ブラウザに表示されているDOM要素をマウス操作で移動したいことがあります。
現在ではHTML5対応ブラウザが用意しているドラッグアンドドロップ用のAPIが使えます1。2008年から2010年にかけて、各種ブラウザはドラッグアンドドロップAPIをサポートしました2。
ブラウザがドラッグアンドドロップをサポートする前も、JavaScriptを使ってマウス操作でのDOM要素移動を実現していたライブラリがありました。そのひとつにjQueryUIのdraggable
ウィジェット3があります。jQueryUIは2008年頃から2012年頃に最も盛んに開発されていました。4
この2つの「マウス操作でのDOM要素移動方法」の併用は考慮されていません。jQueryUIのdraggable
ウィジェットを適用したDOM要素の子要素に、HTML5 ドラッグアンドドロップAPIを、素直に使うと、動きません。

逆に、HTML5 ドラッグアンドドロップAPIを適用したDOM要素の子要素に、jQueryUIのdraggable
ウィジェットを適用しても、特に問題は起きません。
単体での使い方
次にそれぞれの使用方法を見ていきましょう。
jQueryUIのdraggableウィジェット
実に簡単に使えます。次のようにjQueryオブジェクトのdraggable関数を呼びます。
$('div').draggable()
対象DOM要素の上でマウスボタンを押下してから、マウスを移動すると、対象DOM要素が移動します。
https://jqueryui.com/draggable/ のデモがわかりやすいです。
HTML5 ドラッグアンドドロップAPI
こちらも簡単です。draggable属性をtrueにします。
<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を適用します。
<div>
<span id="abc" draggable="true">ABC</span>
</div>
親要素のdivにjQueryUIのdraggable
ウィジェットを適用します。
$('div').draggable()
spanを移動しようとしても、HTML5のドラッグアンドドロップは開始しせず、ゴースト画像は表示されません。親要素のdivがjQueryUIのdraggable
のドラッグ動作をします。
なぜでしょうか?
原因
dragstartイベントが発火しない
HTML5のドラッグアンドドロップの開始イベントdragstart
が発火していません。
次のようにspan要素にdragstart
イベントのイベントリスナーを登録します。
document.querySelector('#abc').addEventListener('dragstart', () => {
console.log('hi')
})
spanを、マウス操作で移動しようとしても、開発コンソールには、何も出力されません。このイベントリスナーは実行されません。
jQueryUIのdraggable
ウィジェットはmousedown
イベントのデフォルト処理を止める
jQueryUIにはマウスイベントを管理するための、ui.mouse
ウィジェットがあります。このウィジェットの_mouseDown
関数は、mousedownイベントに対してpreventDefault
関数を呼んで、デフォルト処理を止めています5。
draggable
ウィジェットはui.mouse
ウィジェットを使っています。draggable
ウィジェットを適用したdivへのmousedown
イベントのデフォルト処理を止めます。
なぜmousedown
イベントのデフォルト処理を止めると、dragstart
イベントが発火しなくなるのでしょうか?
mousedown
イベントとdragstart
イベントにはどのような関係があるのでしょうか?
dragstartイベントはmousedownイベントとそれに続くmousemoveイベント
WHATWGのHTML仕様のドラッグアンドドロップの章6に次のような記述があります。
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があります。
<span id="abc" draggable="true">ABC</span>
このときHTML5 ドラッグアンドドロップは動きます。
次のようなイベントリスナーを登録して、mousedown
イベントのデフォルト処理を止めます。
document.querySelector('#abc').addEventListener('mousedown', (e) => {
e.preventDefault()
})
ドラッグアンドドロップは動かなくなります。dragstart
が発火するためには、まずmousedown
イベントが発火し、さらにデフォルト動作が実行される必要があります。
jQueryUIのdraggable
ウィジェットは、親要素のmousedown
イベントのデフォルト処理を止めているだけです。子要素のmousedown
イベントまで、デフォルト処理が止まるのはなぜでしょうか?
イベントバブリング
バブリング と キャプチャリングに説明があるように、あるDOM要素に発火したイベントは、次に親要素のイベントリスナーを呼び出します。
子要素で発火したmousedown
イベントは、親要素のイベントリスナーを呼び出します。親要素のイベントリスナーは、子要素のイベントのデフォルト動作を止められます。
次のHTMLは、子要素がHTML5 ドラッグアンドドロップできます。
<div>
<span id="abc" draggable="true">ABC</span>
</div>
親要素のイベントリスナーでmousedown
イベントのデフォルト動作を止めます。
document.querySelector('div').addEventListener('mousedown', (e) => {
e.preventDefault()
})
ドラッグアンドドロップは動かなくなります。
イベントリスナーの実行順序
DOM要素をマウス操作で移動しようとしたとき、
- 子要素の
dragstart
のイベントリスナー - 親要素の
mousedown
のイベントリスナー
はどちらが先に実行されるでしょうか?子要素のイベントリスナーが先に実行されることはないのでしょうか?
dragstart
が発火するには、mousedown
イベントが発火したあとに、mousemove
イベントが発火する必要があります。ブラウザの実装は確認していませんが、大抵のブラウザでは、「親要素のmousedown
のイベントリスナー」が先に実行されそうです。
結論
親要素のmousedown
イベントのイベントリスナーがデフォルト動作を止めているため、dragstart
イベントが発火していません。
対策
子要素のmousedown
イベントのバブリングを止める
イベントバブリングはstopPropagation関数をつかえば止められます。
<div>
<span id="abc" draggable="true">ABC</span>
</div>
次ように子要素のmousedown
イベントリスナーでイベントのバブリングを止めます。
$('div').draggable()
document.querySelector('#abc').addEventListener('mousedown', (e) => {
e.stopPropagation()
})
親要素のjQueryUIのdarggable
ウィジェットを使ったマウス操作での移動と、子要素のHTML5 ドラッグアンドドロップAPIを使った移動が併用できます。
これで既にjQueryUIのdarggable
ウィジェットを適用した要素のなかに、新しくドラッグアンドドロップ操作を追加しなければいけなくなっても安心です。
まとめ
今回のエスパーポイントは、
-
dragstart
イベントが発火しない現象 -
mousedown
イベントのデフォルト動作を止めているロジック
両者の、関係を感知するところです。
なぜこのようなエスパーができるのでしょうか?「ドラッグスタートイベントはマウスダウンイベントとマウスムーブイベントの組み合わせである」と知っていたからです。
Reactive Extensionsを使ったWPFのドラッグアンドドロップをまとめる - Qiita
で書いたように、過去にマウスダウンイベントとマウスムーブイベントを組み合わせてドラッグアンドドロップを実装した経験があります。この経験が生きました。WPFとブラウザは実行環境は違いますが、ドラッグアンドドロップの概念は一緒でした。