【Sassで作るCssミニゲーム〜番外編】親子要素でonclickなどのイベントをどちらか一方に指定する方法


2021/07/11
蛸壺の技術ブログ|親子要素でonclickなどのイベントをどちらか一方に指定する方法

html要素が階層構造になっている場合で、たまに別々のonclickイベントをもつ親子要素が重なってクリックされるときがあります。

当然これらの要素上でクリックが起こると両方が反応してしまうため、親子関係関係なくonclickイベントで指定した関数がトリガーされてしまいます。

クリックイベントが重なっても、親/子のクリックイベントのみ発火したい/したくないをCSSで選択的に制御する方法を考えてみましょう。


子要素のイベントを抑制する

階層構造(親子関係)のある要素に対して、子要素に設定されているイベントを起こしたくない時には、pointer-events属性を適切に切り替えることで、親要素の方に設定されているイベントを優先させることができます。

            
            /* イベントを発生させたくない */
.hoge {
   pointer-events: none;
}

/* イベントを発生させたい(デフォルト) */
.piyo {
   pointer-events: auto;
}
        
例えば以下のデモプログラムで、子要素にpointer-events:auto(デフォルト)pointer-events:noneを振り分けたクリックイベントの挙動の違いを示してみましょう。

このサンプルデモのhtml部分だけ抜き出したコードは以下になります。

            
            <form>
    <label for="pointer-events-msg">クリックイベント: </label>
    <input type="text" id="pointer-events-msg" name="pointer-events-msg" value="" />
    <input type="reset" value="リセット">
</form>
<p>pointer-events:</p>
<div id="parent">
    <a id="child1">auto</a>
    <a id="child2">none</a>
</div>
<script>
    document.getElementById("parent").addEventListener("click", (e) => {
        document.getElementById("pointer-events-msg").value += 'PARENT! ';
    });
    document.getElementById("child1").addEventListener("click", (e) => {
        document.getElementById("pointer-events-msg").value += 'CHILD! ';
    });
    document.getElementById("child2").addEventListener("click", (e) => {
        document.getElementById("pointer-events-msg").value += 'CHILD! ';
    });
</script>
        
スタイリングしているscssコードは、

            
            #parent {
    width: 240px;
    background-color: darkgray;
    #child1 {
        display: block;
        width: 100%;
        height: auto;
        color: darkred;
        background-color: rgb(224, 241, 127);
        //pointer-events: auto;でも同じ
    }
    #child2 {
        display: block;
        width: 100%;
        height: auto;
        color: darkred;
        background-color: rgb(122, 148, 233);
        pointer-events: none; //👈子要素のイベントを抑制
    }
}
        
デモプログラムの各要素をクリックすると分かるように、pointer-events: noneを指定した子要素では、狙い通りにイベントが発生しないようにできています。


より高度なイベントの抑制

場合によっては親子関係のある要素でも、親要素のもつイベントをスキップして、子要素からイベントのみを発生させたいということもあります。

DOM要素に設定されているイベントのトリガーされる順序をより自由に制御する場合には、
イベントフェーズという概念をしっかり理解しておく必要があります。

イベントフェーズ

まずはイベントフェーズモデルを示した模式図を以下に示しました。

合同会社タコスキングダム|蛸壺の技術ブログ

このモデルはwindowオブジェクトから始まるDOMツリー内でのイベントの検出順序を表したものです。

まずhmtl内のDOMイベントが発生すると、以下の3つのイベント伝播の順に検知される仕組みがあります。

            
            バブリングフェーズ(デフォルト):
    DOMツリーで下層から上層へボトムアップ式でイベントを検出
キャプチャフェーズ:
    DOMツリーで上層から下層へトップダウン式でイベントを検出
ターゲットフェーズ:
    特定のイベント発生要素を検出。(今回は説明しない)
        
DOMツリーでのイベント検知の基本は、バブリングフェーズです。これにより子要素に仕込まれたイベントが優先して発火され、その処理を完了した後、上層に向かって次のイベントを順次処理をしていくことを意味しています。

キャプチャフェーズはその真逆で、上層のDOMに仕込まれたイベントから順に発火・処理を行い、末端の要素が最後に処理されます。

イベントのバブリング/キャプチャの切り分けは、
addEventListenerメソッドの第三引数で予め登録することで利用可能です。

            
            //👇バブリングフェーズ(第三引数はデフォルトでfalseなので省略可)
document.getElementById("hoge").addEventListener("click", (e) => {
    // Do something
}, false);

//👇キャプチャフェーズ(第三引数はデフォルトでfalseなので省略可)
document.getElementById("piyo").addEventListener("click", (e) => {
    // Do something
}, true);
        
この違いを簡単に以下のデモで試してみましょう。

こちらもクリックしていただければ分かるように、キャプチャフェーズの場合には子要素のイベントが処理された後に親要素のイベントが処理されます。対して、バブリングフェーズのほうはイベントの実行順序が逆転します。

このデモのhtmlのソースコードを抜粋したものが以下になります。

            
            <form>
    <label for="msg-test">親が先?子が先?: </label>
    <input type="text" id="msg-test" name="msg-test" value=""/>
    <input type="reset" value="リセット">
</form>
<div id="parent1">
    <a id="child1">キャプチャフェーズで発火</a>
</div>
<div id="parent2">
    <a id="child2">バブリングフェーズで発火</a>
</div>
<script>
    document.getElementById("parent1").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'PARENT! ';
    },true);
    document.getElementById("child1").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'CHILD! ';
    });
    document.getElementById("parent2").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'PARENT! ';
    },false);
    document.getElementById("child2").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'CHILD! ';
    });
</script>
        
デフォルトであるバブリングフェーズでは、子要素のイベントの次に親要素のイベントが処理されている流れに対して、キャプチャフェーズでは先に親要素のイベントが検出され、その後子要素のイベントが処理されています。

なお、イベントフェーズを切り替えたい場合には、枝分かれしたDOMツリーで分岐の始まる要素のイベントに対してaddEventListenerメソッドの第三引数でtrue/falseを指定します。

上のデモコードの場合で言うと、親要素のイベントに対してバブリングかキャプチャかを選択していたのはそのためで、子要素のイベントのみでキャプチャフェーズ指定しても無効になります。

stopPropagationメソッド

ここからが本題で、組込のevent.stopPropagationメソッドを使えばイベント伝播を抑制することが可能になります。

このstopPropagationによるイベント伝搬の抑制は、バブリングフェーズでもキャプチャフェーズでも利用できますが、どっちのフェーズで使うかで大きく効能が違います。

つまりバブリングフェーズで使えば親要素の持つイベントが抑制され、キャプチャフェーズで使えば子要素が抑制され、これによって親子要素に対してどちらのイベントを発生させるのかを選択することが可能です。

以下のデモでは先程のデモに
stopPropagationメソッドを使った例です。

修正した箇所は以下の部分です。

            
            ...省略
<script>
    document.getElementById("parent1").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'PARENT! ';
        e.stopPropagation(); //キャプチャフェーズにつき、子要素のイベントを抑制
    },true);
    document.getElementById("child1").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'CHILD! ';
    });
    document.getElementById("parent2").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'PARENT! ';
    },false);
    document.getElementById("child2").addEventListener("click", (e) => {
        document.getElementById("msg-test").value += 'CHILD! ';
        e.stopPropagation(); //バブリングフェーズにつき、親要素のイベントを抑制
    });
</script>
        
コード内のstopPropagationメソッドの使い方を見て頂いても分かるように、イベントフェーズの概念をきちんと理解していれば、stopPropagationメソッドを親要素・子要素のどちらのイベントに設定するすると正しく動作するか判断できると思います。


まとめ

今回は階層構造を持ったDOMで、入れ子で仕込んでしまったイベントをどう制御するかを解説してみました。

結論としては、子要素のイベントを発火したくない場合には、cssスタイルからpointer-eventsを付ければ簡単です。

もっと細かくイベントを設定したい場合には、DOMツリーのイベントフェーズの切り分けと、stopPropagationメソッドの併用が必要になってくるかも知れません。

いずれにせよ複雑なイベント発火構造をもつDOMツリーでは後々管理が大変になる恐れもありますので、入れ子構造になるイベントは程々にしておかれる方が宜しいかと思います。


参考サイト

親要素をクリックしたつもりが子要素をクリックしてしまう時の解決方法

親と子のclickイベントが併発しないようにする

DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。