ファイルなどをドロップする要素が子要素を持っていても正しくホバーエフェクトを表示する方法


## 前提

~~このコードは、Windows版 Google Chrome 35.0.1916.114 mで動作チェックを行っており、
**他のブラウザでは一切確認を行っておりません**。~~

<ins>他のブラウザでも動作することを確認しました。</ins>

詳細は[動作確認ブラウザ](#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6)をどうぞ。

## 問題

次のようなコードで、ドロップ対象の要素にクラスを追加しエフェクト(色の変更や影の表示)を発生させている場合、

```html
<div id="target">ここにドロップ!</div>
```

```js
//ファイルのドロップ対象要素
var targetE = document.getElementById('target');

//ドロップ対象要素にファイルが重なった場合、クラスを追加する
targetE.addEventListener('dragenter', function () {
    targetE.className = 'over';
}, false);

//ファイルがドロップ対象要素から出たらクラスを削除する
targetE.addEventListener('dragleave', function () {
    targetE.className = '';
}, false);
```

対象要素が子要素を持つとうまくいきません。

```html
<div id="target">
    <div>ここにドロップ!<br>または、ファイルを選択</div>
    <input type="file" name="file[]" multiple>
</div>
```

```js
//ファイルのドロップ対象要素
var targetE = document.getElementById('target');

//ドロップ対象要素にファイルが重なった場合、クラスを追加する
targetE.addEventListener('dragenter', function () {
    targetE.className = 'over';
}, false);

//ファイルがドロップ対象要素から出たらクラスを削除する
targetE.addEventListener('dragleave', function () {
    targetE.className = '';
}, false);

//クラスの追加/削除が子要素への移動でも発生し、
//色とかの場合、"チカチカ"が発生する
```

実際、私の作っているファイルアップロード部分でこの問題が発生し、どう回避するか悩みました。

## 解法

これを回避するには、以下のようにコードを組むとうまくいきます。

```html
<div id="target">
    <div>ここにドロップ!<br>または、ファイルを選択</div>
    <input type="file" name="file[]" multiple>
</div>
```

```js
//ファイルのドロップ対象要素
var targetE = document.getElementById('target');

//対象要素と子要素とでの移動フラグ
var innerFlag = false;

//dragenterイベントが発生した時、フラグをセット
targetE.addEventListener('dragenter', function () {
    innerFlag = true;
}, false);

//ドロップ対象要素にファイルが重なっている間、クラスを追加する
//また、フラグをリセットする
//(対象要素内での移動によるdragenter -> dragleaveイベントの場合、その間でdragoverイベントは発生しない)
targetE.addEventListener('dragover', function () {
    //フラグをリセット
    innerFlag = false;
    //クラスをセット
    targetE.className = 'over';
}, false);

//ファイルがドラッグされ、
//フラグが未設定(対象要素内での移動により発生したdragleaveイベントではない時)の場合、
//要素から出たらクラスを削除する
targetE.addEventListener('dragleave', function () {
    if (innerFlag) {
        //フラグがセットされている場合、フラグを戻す
        innerFlag = false;
    } else {
        //フラグがセットされていない場合、クラスを削除
        targetE.className = '';
    }
}, false);
```

[サンプル](http://jsbin.com/tesum)

## 原因

### 発生するイベント

この問題が発生する原因は、対象要素内の子要素で`dragenter`イベント及び`dragleave`イベントが発生し、それに反応してしまうのが原因です。

実際に計測したところ、子要素を含んだ要素へのドラッグオーバーでは状況に応じ、
以下の順でイベントが発生していました。

|状況                      |イベント                                   |
|-------------------------|-------------------------------------------|
|対象要素へ外からドラッグ    |対象要素で`dragenter`                       |
|対象要素から子要素へドラッグ|子要素で`dragenter`   → 対象要素で`dragleave` |
|子要素から子要素へドラッグ  |子要素で`dragenter`     → 子要素で`dragleave`|
|子要素から対象要素へドラッグ|対象要素で`dragenter` → 子要素で`dragleave`   |
|対象要素から外へドラッグ    |対象要素で`dragleave`                       |
|子要素へ外からドラッグ      |子要素で`dragenter`                         |
|子要素から外へドラッグ      |子要素で`dragleave`                         |

※最後の「子要素へ外からドラッグ」「子要素から外へドラッグ」は、
　例えばブラウザに重ねた[エクスプローラー](https://ja.wikipedia.org/wiki/Windows_Explorer "Windows Explorer")とのドラッグや、
　`Alt`+`Tab`キーによるウィンドウの切り替え(Windowsの場合)などで発生します。

この全てのイベントでクラスの追加/削除が起こってしまい、エフェクトの発生が断続的に起こってしまうのが原因です。

### テスト

期待する動作としては、要素同士の移動により発生する`dragenter` → `dragleave`でのクラスの変更を行わないようにするのが理想です。
しかし、これがなかなかうまくいきません。

単純に子要素でのイベントの伝播をキャンセルするのは要素数が増えれば大変になりますし、
エクスプローラーからの直接ドラッグに対処出来なくなります。

私が最初に行った対策は、以下のようにフラグを設定することでした。
`dragenter`の後に発生する`dragleave`でクラスを削除しないようにしたわけです。

```js
//ファイルのドロップ対象要素
var targetE = document.getElementById('target');

//対象要素と子要素とでの移動フラグ
var innerFlag = false;

//dragenterイベントが発生
targetE.addEventListener('dragenter', function () {
    //クラスを追加
    targetE.className = 'over';
    //フラグをセットし、直後に発生するdragleaveイベントでのクラス削除を抑制する
    innerFlag = true;
}, false);

//dragleaveイベントが発生した時、
//フラグが未設定(dragenterイベントの直後に呼ばれた場合ではない時)の場合、
//クラスを削除する
targetE.addEventListener('dragleave', function () {
    if (innerFlag) {
        //フラグがセットされている場合、フラグを戻す
        innerFlag = false;
    } else {
        //フラグがセットされていない場合、クラスを削除
        targetE.className = '';
    }
}, false);
```

この場合、確かに無駄なクラスの削除は発生しません。
しかしこれだと、対象要素へドラッグした後、子要素へ入れないまま外にファイルをドラッグした時にエフェクトが消えないという問題があります。
つまり、

|状況                               |イベント                                   |
|----------------------------------|-------------------------------------------|
|対象要素へドラッグ後、外へドラッグ    |対象要素で`dragenter`  → 対象要素で`dragleave`|

この場合に、エフェクトが消えずに残ってしまいます。
以下にサンプルを用意しました。色の付いた部分(子要素)に重ならないようファイルをドラッグし、
外へ出すと、エフェクト(緑に変わったボーダー)が戻らなくなります。

[Drag effect test](http://jsbin.com/suteti)

コレ以外に、フラグのセットを子要素のみで行ったりなどいろいろやってみましたが、どうもうまくいきませんでした。

### ブレイクスルー

ここで、他のドラッグイベントも計測したところ、ある事に気が付きました。

要素同士の移動により発生する`dragenter` → `dragleave`の間では他のイベントが発生していません。
具体的には、間で`dragover`イベントが発生していませんでした。
一方、[テスト](#%E3%83%86%E3%82%B9%E3%83%88)で出した問題の`dragenter` → `dragleave`では、間で`dragover`イベントが発生しています。

つまり、`dragenter` → `dragleave`の間で`dragover`イベントが発生していない場合は、
子要素間の移動によるものと考え、無視できるわけです。

### テスト 改

これを踏まえ、`dragenter`でセットしたフラグを`dragover`で上書きし、
`dragleave`に備えるように改造したものが[解法](#%E8%A7%A3%E6%B3%95)のコードになります。

無視したい子要素間の移動では`dragover`によりフラグが上書きされないため、
直後に発生する`dragleave`でフラグによりクラスが追加されません。

一方、それ以外の場合は、間で`dragover`によりフラグが上書きされるので、
後に発生する`dragleave`で正常にクラスが削除されます。

```js
//ファイルのドロップ対象要素
var targetE = document.getElementById('target');

//対象要素と子要素とでの移動フラグ
var innerFlag = false;

//dragenterイベントが発生した時、フラグをセット
targetE.addEventListener('dragenter', function () {
    //フラグをセット
    innerFlag = true;
}, false);

//ドロップ対象要素にファイルが重なっている間、クラスを追加する
//また、フラグをリセットする
//(対象要素内での移動によるdragenter -> dragleaveイベントの場合、その間でdragoverイベントは発生しない)
targetE.addEventListener('dragover', function () {
    //フラグを上書きする
    innerFlag = false;
    //クラスをセット
    targetE.className = 'over';
}, false);

//ファイルがドラッグされ、
//フラグが未設定(対象要素内での移動により発生したdragleaveイベントではない時)の場合、
//要素から出たらクラスを削除する
targetE.addEventListener('dragleave', function () {
    if (innerFlag) {
        //フラグがセットされている場合、フラグを戻す
        innerFlag = false;
    } else {
        //フラグがセットされていない場合、クラスを削除
        targetE.className = '';
    }
}, false);
```

ちなみに、クラスの追加を`dragenter`ではなく`dragover`でやるよう変更した理由として、
…よく分かりませんがこうしたほうがうまくいくからです。
`dragenter`だとたまにうまくいかない事がありまして…

[JavaScript - HTML5でファイルをドラッグして読み込むやつ - Qiita](http://qiita.com/ginpei/items/5cebb14e210fc0770e94#2-3) でも、`dragover`でクラスを追加するとあるため、
おそらく正攻法であろうこっちを採用しています。

## 動作確認ブラウザ

いずれもWindows 7 Home Premium 64bitでの確認となります。

* Internet Explorer 11.0.9600.17239
* Internet Explorer 10 (F12 開発者ツール)
* Internet Explorer 9 (F12 開発者ツール)
* Google Chrome 35.0.1916.114 m
* Google Chrome 36.0.1985.143 m
* Firefox 31.0
* Opera 12.17
* Safari 5.1.7 (7534.57.2)
* Sleipnir 4.1.2.4000 (Webkit)
* Sleipnir 4.1.2.4000 (IE10)
* Lunascape 6.8.7 (WebKit)
* Lunascape 6.8.7 (Trident)
* Lunascape 6.8.7 (Gecko)
* Sogou browser 5.0.9.13085
* Maxthon 3.4.5.2000
* Comodo Dragon 33.1.0.0
* CoolNovo 2.0.7.11 (Chrome Mode)
* CoolNovo 2.0.9.20 (Chrome Mode)
* RockMelt 0.16.91.483
* SRWare Iron 21.0.1200.0
* Superbird 24.0.1312.57 (178923)
* Pale Moon 19.0.2
* Pale Moon 24.7.1 (x86)
* CyberFox 19.0.2
* SeaMonkey 2.21

## 最後に

ここまで読んで下さり、ありがとうございます。
~~この投稿の内容はGoogle Chromeでのみテストした結果に基づくものであるため、~~
<ins>この投稿の内容はWindows環境でのみテストした結果に基づくものであるため、</ins>
他の環境では違う結果によりうまく動作しないかも知れません。

また、素人思考のトライ&エラーから導き出したコードであるため、
もっとエレガントで素晴らしいコードがあるかもしれません。
そういった意見、指摘、疑問などありましたら、コメントにて受け付けます。

また文章力の低さ故、伝わりにくい文章かも知れません。
編集リクエストも受け付けます。