伝播ルート外でイベントを補足する

自分のgistを漁っていたら**伝播ルート外でイベントが発生したことを知らせる**カスタムイベントを発掘しました。

利用シーンがいまいち思い浮かばないのですが、実用性はさておき、面白いなと思ったのでネタとして投稿します。

## サンプル
言葉で言われても意味不明だと思うのでサンプルです。

<p data-height="500" data-theme-id="0" data-slug-hash="NExYKE" data-default-tab="result" data-user="tatesuke" data-pen-title="Out Of Event" class="codepen">See the Pen <a href="https://codepen.io/tatesuke/pen/NExYKE/">Out Of Event</a> by tatesuke (<a href="https://codepen.io/tatesuke">@tatesuke</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


* AをクリックするとB,C,D,Eがオレンジ色になります  
* BをクリックするとC,D,Eがオレンジ色になります  
* CをクリックするとD,Eがオレンジ色になります
* DをクリックするとB,C,Eがオレンジ色になります
* EをクリックするとB,Cがオレンジ色になります


要するに、クリックイベントの**伝播ルート上にないエレメント**がオレンジ色になります。

このサンプルでは。以下のように**クリックイベントを捕捉出来ないエレメントに対して**処理を記載しています。

```js
// 要素外がクリックされたら背景をオレンジにする
elem.addEventListener("outofclick", function (e) {
    elem.style.backgroundColor = "#FBA848";
    console.log("outOfClick:" + elem.id);
});
```


## 仕組み
仕組みは単純です。

あるエレメントでイベントが発生すると、ツリー上の上位エレメントまで伝搬します。
<img width="408" alt="無題2.png" src="https://qiita-image-store.s3.amazonaws.com/0/29682/677dae49-3342-a0a0-5664-917b7ceef660.png">

上位まで伝搬したら、今度は伝播ルートを逆順にたどってマークをつけていきます。
<img width="408" alt="無題3.png" src="https://qiita-image-store.s3.amazonaws.com/0/29682/fc6a14c8-06bf-9f62-6017-c7d2700fe7f5.png">

マークを付け終わった後、上位ノード配下の全てのエレメントに対して次の操作を行います。

* マークが付いていないエレメントであれば、伝搬ルート外でイベントが起きたことをカスタムイベントで知らせる
* マークがついているエレメントであれば、マークを消す

<img width="408" alt="無題.png" src="https://qiita-image-store.s3.amazonaws.com/0/29682/1417de45-a613-4db2-fedb-bd5b07a78c74.png">


## ソースと使い方
仕組みが単純ならソースも単純です。まずできるだけ上位のエレメントにカスタムイベントを発行させるイベントリスナを登録しておきます。

```js
// 上位エレメントのイベントハンドラにtriggerOutOfEventを登録する
window.addEventListener("click", triggerOutOfEvent)

// 捕捉例外
function triggerOutOfEvent(originalEvent) {
    var rootElem = (originalEvent.currentTarget.dataset) ? originalEvent.currentTarget : document.body;
	
	// イベント伝播ルート上のエレメントをマーク
    var currentElem = originalEvent.srcElement;
    while ((currentElem !== rootElem) && (currentElem !== null)) {
        currentElem.dataset.isOnRoute = "true";
        currentElem = currentElem.parentElement;
    }

	// カスタムイベント生成
    var outOfEvent = new CustomEvent("outof" + originalEvent.type, {
        detail: {
            srcEvent: originalEvent
        }
    });

	// イベント伝播ルート上のエレメント以外に対してカスタムイベントをディスパッチ
    [].forEach.call(rootElem.querySelectorAll("*"), function (elem) {
        if (elem.dataset.isOnRoute === "true") {
            elem.dataset.isOnRoute = null;
        } else {
            elem.dispatchEvent(outOfEvent);
        }
    });
}
```

次に、OutofEventを検知したいエレメントにイベントリスナを登録します。

```js
elem.addEventListener("outofclick", function(e) {
  elem.style.backgroundColor = "#FBA848";
  console.log("outOfClick:" + elem.id);
});
```

これだけです。


### ポリフィル
古いブラウザだとカスタムイベントが使えない可能性があります。そんなときはポリフィルです。

```js
// CustomEventのポリフィル
(function () {

    if (typeof window.CustomEvent === "function") return false;

    function CustomEvent(event, params) {
        params = params || { bubbles: false, cancelable: false, detail: undefined };
        var evt = document.createEvent('CustomEvent');
        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
        return evt;
    }

    CustomEvent.prototype = window.Event.prototype;

    window.CustomEvent = CustomEvent;
})();
```
## 注意点
イベント通知をわざわざ広範囲に広げているので性能が気になるところですね。
まぁ本格的に使用することなんてないと思うけど。