2
1

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 3 years have passed since last update.

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

Posted at

結論

子要素の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を、素直に使うと、動きません。

スクリーンショット 2020-05-14 23.01.48.png

逆に、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要素をマウス操作で移動しようとしたとき、

  1. 子要素のdragstartのイベントリスナー
  2. 親要素の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ウィジェットを適用した要素のなかに、新しくドラッグアンドドロップ操作を追加しなければいけなくなっても安心です。

まとめ

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

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

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

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

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

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

  1. https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API

  2. https://caniuse.com/#feat=dragndrop

  3. https://jqueryui.com/draggable/

  4. https://jqueryui.com/changelog/

  5. https://github.com/jquery/jquery-ui/blob/1.12.1/ui/widgets/mouse.js#L130

  6. https://html.spec.whatwg.org/multipage/dnd.html#dnd

2
1
1

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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?