Chapter 10

イベント

OJK
OJK
2021.08.25に更新

準備

勉強用フォルダの下に「10」という名前で作業フォルダを作成して、以下のファイルを作成し、ライブプレビュー画面(ブラウザ)とコンソールを準備してください。

雛形コード
index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Chapter 10: イベント</title>
  <style>
    .baseStyle {
      box-sizing: border-box;
      width: 200px;
      padding: 10px;
      border-style: solid;
      font-size: 16px;
    }
    .myStyle {
      border: solid 3px gold;
      font-weight: bold;
    }
    .newStyle {
      color: firebrick;
    }
  </style>
</head>
<body>
  <button>ボタン</button>

  <p id="target" class="baseStyle">
    ここのスタイルが変わります
  </p>

  <img src="https://github.com/ojklab.png" alt="画像" width="200">

  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

const button = document.querySelector('button');
console.log(button);

イベントの取得

JavaScript を使えば、ブラウザに対するユーザのさまざまな操作が取得できます。例えば以下のような操作が取得できます。

  • マウス操作(クリックやポインタの移動など)
  • キー操作(キーの押下や入力など)
  • ブラウザ操作(読み込みやスクロールなど)

これらのことをまとめて “イベント/event” といいます。例えば、マウスクリックされることを「クリックイベントが発生する」と表現します。イベントが「発火する」という言い方もされることがあります。

これまで本書で例示してきたサンプルコードは上から下に向かって順番に実行されていくものでした。例外といえば、関数が呼び出されると関数定義のところに処理が飛ぶくらいでしょうか。
しかし本来の JavaScript プログラミングでは、「何かイベントが起こったら、それに対応する処理を実行する」という形で記述していきます。上から下に順に処理が進むのではなく、イベントが発生するのをじっと待ち構えておいて、イベントが起こったら用意しておいた処理を実行するのです。

このような仕組みを “イベント駆動型/event driven” といい、JavaScript プログラミングは “イベント駆動型プログラミング” ともいわれます。イベント駆動型プログラミングでは、「◯◯ がクリックされたとき」というようなイベントに対応する処理を関数宣言の形で記述します。

これまでは、以下のような感じでコード内に関数呼び出しを置いていました。

// 関数宣言
function 関数() {
  // 呼び出されたときに実行される
}

関数();  // 関数呼び出し

イベント駆動型では、関数呼び出しの代わりに、次のように関数をイベントに対応づけします。

function 関数() {
  // イベントが発生したときに実行される
}

登録(イベント, 関数); // イベントと関数を対応づける

// あとはイベントが発生するのをひたすら待つ

イベント駆動型プログラミングでは普通の関数呼び出しを使わないということではありません。特にメソッド呼び出しは普通に使います。ただ、プログラミングの中心的な作業が、上から下に順番に命令を書いていくスタイルから、イベント毎に関数を登録していく作業になるということです。

また、イベントに登録する関数を使い回すことは少ないので、関数宣言はせず、無名関数を直接登録するのが一般的です。ただし、後からイベントを削除する必要のあるときは、関数宣言する必要があります(後述)。

登録(イベント, function {
  // イベントが発生したときに実行される処理
});

無名関数ではアロー関数もよく使われるのでした。イベントに関しては function 関数式とアロー関数で一部の振る舞いが異なるのですが、本書ではアロー関数を使っていきます。

登録(イベント, () => {
  // イベントが発生したときに実行される処理
});

ここまでの内容で躓いた人は、もう一度、関数のチャプターを復習してみてください。特に、関数式が今回はたくさん登場します。

なお、関数呼び出しのようにその場で実行されるのではなく、イベントなどに応じて呼び出される関数のことを “コールバック関数/callback function” といいます。後から呼び出される(コールバックされる)関数という意味です。

そして、コールバック関数の中でも、本チャプターでこれから学ぶ「イベントと対応づけられる関数」のことを “イベントリスナー/event listener” といいます[1]。イベントが生じるかどうか聞き耳を立てている者という意味です(たぶん)。
以下、イベントと対応づけられる関数のことを “イベントリスナー” と表記するので、しっかり頭を切り替えてください。脳内で「関数」と読み替えても何ら支障はありません。

イベントリスナーを追加する

では、実例を見ていきましょう。

まず、前述の模式コードで「登録()」と表現していた関数(メソッド)は、addEventListener メソッド になります。addEventListener はエレメント(Element オブジェクト)が持っているメソッドです。

エレメント.addEventListener(イベント名, イベントリスナー);

例えばクリックに何かしら処理を対応づけるとして、何をクリックしたときの話なのかを指定しなくてはなりません。この「何を」に当たるのは HTML 要素ですが、その要素が自分自身にイベントリスナー(関数)を追加するので「add EventListener」です。

雛形コードを使って、button 要素をクリックしたときに p 要素のスタイルを変更する例を見ていきます。まずは button 要素をエレメントとして呼び出します。

HTML
  <button>ボタン</button>

  <p id="target" class="baseStyle">
    ここのスタイルが変わります
  </p>
const button = document.querySelector('button');
console.log(button);  // → <button>ボタン</button>

マウスクリックのイベント名は「click」です。文字列リテラル 'click' で指定します。
button 要素に対する click イベントが発生したら、button 要素の下にある p 要素の class を切り替えてスタイルを変更するようにしてみます。myStyle クラスの有/無を classList.toggle メソッドで切り替えます。

button.addEventListener('click', () => {
  const p = document.getElementById('target');
  p.classList.toggle('myStyle');
});

ボタンをクリックする毎にスタイルが切り替わりますね。少しアプリケーションらしくなってきたでしょうか。開発者ツールの Element タブで p 要素を観察すると、ボタンを押すたびに myStyle が付いたり外れたりする様子が見られます。

複数のイベントリスナーを関連付ける

同じ HTML 要素へのイベントに対して、複数のイベントリスナーを関連付けることができます。

さきほどのサンプルコードに以下を追加してください。同じ button 要素に対して click イベントをもう一つ追加しています。

button.addEventListener('click', () => {
  const p = document.getElementById('target');
  p.classList.toggle('newStyle');
});

ボタンをクリックすると黄色い枠が付くだけでなく、文字の色が赤色になったかと思います。開発者ツールの Element タブで class が二つ追加されていることが確認できます。

複数のイベントを関連付ける

一つの HTML 要素に複数のイベントを関連付けることもできます。

マウスポインタが button 要素に乗ったときに発生する mouseenter イベントを使って、p 要素の文字サイズを変更してみましょう。さきほどの click イベントの処理の続きに下記を記述してください。

button.addEventListener('mouseenter', () => {
  const p = document.getElementById('target');
  p.style.fontSize = '24pt';
});

これだと、button 要素からマウスポインタが外れても大きくなったままですね。マウスポインタが要素を抜けたときに発生する mouseleave イベントに、フォントサイズを元に戻すイベントリスナーを追加しましょう。

button.addEventListener('mouseleave', () => {
  const p = document.getElementById('target');
  p.style.fontSize = '16pt';
});

同じ処理を、何度も p エレメントを取得せずに以下のように記述することも可能です。

const p = document.getElementById('target');

button.addEventListener('click', () => p.classList.toggle('myStyle'));
button.addEventListener('click', () => p.classList.toggle('newStyle'));
button.addEventListener('mouseenter', () => p.style.fontSize = '24pt');
button.addEventListener('mouseleave', () => p.style.fontSize = '16pt');

アロー関数の中身が代入演算子 = を含まない 1 行のコード、つまり「式」になるので { } は省略できます。文ではなく式になるので、文の終端を意味するセミコロンも忘れずに削除します。

Event オブジェクト

マウスクリックは button 要素にしか使えないわけではありません。ほとんどの HTML 要素で click イベントを受け取ることができます。

例えば、img 要素をクリックすると画像を切り替えるようにしてみましょう。

HTML
<img src="https://github.com/ojklab.png" alt="画像" width="200">
JavaScript
const img = document.getElementsByTagName('img')[0];

img.addEventListener('click', () => {
  img.src = 'https://github.com/ugok-girls.png';
});

さきほどのサンプルコードとあまり変わりませんが、img エレメントのイベントリスナーの中で img エレメントを使用しています。

このように、addEventListener を呼び出したエレメント自体をイベントリスナーの中でも使用するときは、別の表現を使うことができます。次のコードを見てください。

JavaScript
img.addEventListener('click', (ev) => {
  ev.currentTarget.src = 'https://github.com/ugok-girls.png';
});

定数 img を使っていたところが ev.currentTarget に置き換わっています。
この ev という変数がどこから来たかというと、アロー関数の引数に指定されています。この引数の中身は Event オブジェクト であり、イベントが発生したときにイベントリスナーに渡されます。

currentTarget プロパティ は Event オブジェクトのプロパティの一つで、イベントリスナーが対応づけられたエレメントを表します。変数名は ev でなくても構いません(他には e や event という変数名がよく使われます)。

コードが長くなってしまうので img でいいじゃないか…という気もしますが、例えば、エレメントを呼び出して直接 addEventListener をつなげることもあります。

document.querySelector('img').addEventListener('click', (ev) => {
  const img = ev.currentTarget;  // 短い名前を付ける
  img.src = 'https://github.com/ugok-girls.png';
})

その場合、イベントリスナーの中でエレメントにアクセスするのに毎回 document.querySelector('img') などと書いていては長くなりますし、処理も重くなってしまうので、ev.currentTarget を使うほうがよいでしょう。ev.currentTarget は長い!ということなら、短い名前を付ければ OK です。

マウスポインタの位置を取得する

また、Event オブジェクトは他にもいろいろと便利なプロパティを持っています。
例えば、マウスクリック系イベント(click/dblclick/mouseup/mousedown)の際にはクリックされた座標を取得できます[2]。座標系の異なるプロパティがいくつか用意されています。

clientX/clientY プロパティはブラウザ座標系でのクリック座標を表します。
以下のコードは、img 要素をクリックするとブラウザの表示領域(タブや アドレスバーを除いた部分)における相対的なクリック座標がコンソールに表示されます。座標系の原点はブラウザの左上角で、y 軸は下にいくほど値が大きくなります。

JavaScript
img.addEventListener('click', (ev) => {
  // ブラウザ座標系でのクリック位置
  console.log(ev.clientX, ev.clientY);
});

offsetX/offsetY は要素座標系におけるクリック座標を表します。
img 要素の左上角を原点とするクリック座標を取得して、img 要素の下に表示してみましょう。座標値は p 要素として追加することにします(クリックするたびに新しい p 要素を追加します)。

img.addEventListener('click', (ev) => {
  const p = document.createElement('p');  // p要素を生成

  // 要素座標系でのクリック位置
  p.textContent = `X:${ev.offsetX} Y:${ev.offsetY}`;

  // 親要素はbody要素、img要素の下に挿入
  document.body.insertBefore(p, img.nextElementSibling);
});

img 要素の下にエレメントを追加するには、img 要素の親要素から insertBefore メソッド を呼び出す必要があります。
今回、img 要素の親要素は body 要素ですが、body 要素のエレメントだけは特別に document.body として最初から用意されているのでした。img 要素の前(before)ではなく後に p 要素を挿入したいので、img エレメントに nextElementSibling(もしくは nextSibling)プロパティを付けます。

その他の座標系もまとめて以下に示しておきます。下に行くほど広い範囲の座標系になります。

座標系 プロパティ名
要素座標系 offsetX/offsetY
ブラウザ座標系 clientX/clientY
スクリーン座標系 screenX/screenY
ページ座標系 pageX/pageY

ページ座標系は、ブラウザからはみ出して見えない部分も含めたウェブページの左上角を原点とする座標系です。スクリーンは PC の画面のことです。offsetX/offsetY は要素を入れ子にするとややこしくなってくるのですが、本書では説明を割愛します(参考)。

エレメント以外のイベント対象

ここまで、イベントを受け取るのはエレメント(Element オブジェクト)であるという前提で書いてきましたが、他にも window オブジェクト(ブラウザ)や document オブジェクト(HTML ドキュメント)が受け取るイベントがあります。

したがって、冒頭で紹介した addEventListener の構文は次のようになります。イベントを受け取るのはエレメントだけではないので「イベント対象」に変えました。

イベント対象.addEventListener(イベント名, イベントリスナー);

例えば、ブラウザをリサイズしたときに発生する resize イベントを受け取るのは window オブジェクトです。以下のコードを好きなところに記述し、ブラウザの大きさを変えてみてください。p 要素の背景色が gold 色になります。

window.addEventListener('resize', () => {
  const p = document.getElementById('target');
  p.style.backgroundColor = 'gold';
});

また、HTML ドキュメントの読み込みが完了したという DOMContentLoaded イベント を受け取るのは document オブジェクトです。

JavaScript の目的は DOM ツリーの操作なので、本来は HTML ドキュメントの読み込み完了を待ってから JavaScript を実行すべきです。そのため、ウェブアプリケーション開発の現場では、次のように JavaScript プログラム全体を DOMContentLoaded イベントのイベントリスナーの中に記述する方法もとられます。

document.addEventListener('DOMContentLoaded', () => {
  // ここにJavaScriptのプログラムを書く
});

同様の目的で window オブジェクトの load イベントを利用しているケースもあります。しかし、load イベントは画像やスタイルシートまで読み込んでから発生するので、画像のデータ量が大きいときなどに JavaScript の開始が遅れてしまうことがあります。したがって、通常は document オブジェクトの DOMContentLoaded イベントを使います。

各オブジェクトで受け取れるイベントは他にもたくさんあります。ここでは紹介しきれないので、欲しいイベントが出てきたときは都度調べるようにしてください。

規定の動作をブロックする

いくつかの「イベント対象とイベントのペア」には規定の動作が設定されているものがあります。例えば、a 要素をクリックするとリンク先に飛んだり、ラジオボタンをクリックするとマークが付いたり、ブラウザを右クリックするとコンテキストメニューが表示されたりなどです。

JavaScript を使えば、これらの規定動作をブロックすることができます。
まず、ブロックしたい「イベント対象とイベントのペア」に対して addEventListener メソッドを呼び出します。そのイベントリスナーの中で、Event オブジェクト.preventDefault メソッド を呼び出します。

イベント対象.addEventListener(イベント名, (ev) => {
  ev.preventDefault();  // 規定動作を止める
})

試しに a 要素に対するクリックの規定動作(リンク先に飛ぶ)を止めてみましょう。
雛形コードの中に a 要素がないので、まずは JavaScript で a 要素を新規追加します。以下のコードをイベントリスナーの外に書いて、正しくリンク先に飛ぶか確認してください。

// a要素の生成
const a = document.createElement('a');
a.href = 'https://zenn.dev/ojk';
a.textContent = 'リンク先に飛びます';

// script要素の上に追加
const script = document.querySelector('script');
document.body.insertBefore(a, script);

次に、ボタンを押すと a 要素の規定動作をブロックするようにします。button エレメントの addEventListener メソッドの中で、さらに a エレメントの addEventListener メソッドを呼び出します。ついでに a 要素のテキストも変更します。

button.addEventListener('click', () => {
  a.textContent = 'リンク先に飛びません';
  a.addEventListener('click', (ev) => {
    ev.preventDefault();  // 規定動作をブロック
  });
});

ボタンを押すと、リンクのテキストが「リンク先に飛びません」に変わり、クリックしても何も起こらなくなります。

もう一つ例を示します。ブラウザを右クリックするとコンテキストメニューが表示されますが、それをブロックします(p5.js で重宝します)。
こちらは一行です。

document.addEventListener('contextmenu', (ev) => ev.preventDefault());

イベントリスナーの削除

一度設定したイベントリスナーを削除したいときは、removeEventListener メソッド を呼び出します。

イベント対象.removeEventListener(イベント名, イベントリスナー)

イベント対象とイベント名だけでなく、削除したいイベントリスナーも指定しなければなりません。しかし、ここまで例示してきたイベントリスナーは無名関数だったので、あとから名指しで指定することができません。
したがって、あとからイベントリスナーを削除する必要があるときは、イベントリスナーに名前を付けておく必要があります。

アロー関数で名前付き関数を定義するには、関数式をそのまま定数/変数に代入します。

const letPGold = () => {
  const p = document.getElementById('target');
  p.style.backgroundColor = 'gold';
};

そして、この関数 letPGold をイベントリスナーとして登録します。

window.addEventListener('resize', letPGold);

このとき注意してほしいのは、関数 letPGold の後ろの ( ) は付けないということです。後ろに ( ) を付けてしまうと、そこで関数が実行されてしまい、addEventListener メソッドの第 2 引数にはその戻り値だけが渡されてしまいます。

以下は処理の流れのイメージです。関数 letPGold のように return のない関数の戻り値は undefined になります。

()あり
window.addEventListener('resize', 関数()); // 実行されてしまう
↓
window.addEventListener('resize', 戻り値); // 戻り値に置き換わる
↓
window.addEventListener('resize', undefined);
()なし
window.addEventListener('resize', 関数); // 実行されない
↓
window.addEventListener('resize', () => {...}); // 関数の定義に展開される

では、window オブジェクトに設定したイベントリスナー letPGold を、ボタンを押すことで削除するようにしましょう。letPGold は resize イベントに対して設定していたので、removeEventListener メソッドに指定するイベント名も resize にする必要があります。

const button = document.querySelector('button');

button.addEventListener('click', () => {
  window.removeEventListener('resize', letPGold);
});

ボタンを押してからブラウザの大きさを変えると p 要素の背景色は変わらなくなります。

ちょっとわかりにくいので、1 度ボタンを押したらボタンが無効になるようにしてみましょう。
ボタンを無効にするには button 要素に disabled を付けます。button エレメントの disabled プロパティに Boolean 型(true/false)を指定することで有効/無効を切り替えることができます。

button.addEventListener('click', () => {
  window.removeEventListener('resize', letPGold);
  button.disabled = true;  // disabledを有効にする
});

disabled プロパティの場合、true にすると無効になる…というのがちょっとややこしいですね…

addEventListener のオプション

addEventListener メソッドには 3 番目の引数があります。これはオプションで、addEventListener メソッドの振る舞いを制御することができます。省略すると初期値(デフォルト値)が適用されます。

イベント対象.addEventListener(イベント名, イベントリスナー, オプション);

addEventListener のオプションは高度な内容を含むので、一番簡単な「一度だけ実行されたら削除されるイベントリスナー」のみを紹介しておきます。

const button = document.querySelector('button');

button.addEventListener(
  'click',
  () => {
    const p = document.getElementById('target');
    p.classList.toggle('myStyle');
  },
  { once: true }
);

1 行が長くてコードフォーマッタ(Prettier)に改行されてしまいましたが、第 3 引数として { once: true } が設定されていることがわかるでしょうか。
これでボタンを押すたびにスタイルが切り替わっていたものが、一度しか動作しないようになります。オプションを付けたり外したりして動作を確認してみてください。

なお、引数がオブジェクトの形式になっているのは、一つの引数で複数のオプションを設定できるようにするためです。例えば、{ once: true, passive: true } といったように指定できます。オブジェクトを使った引数の記法もよく使われるので頭の片隅に置いておいてください。

最後に イベント名の一覧 を置いておきます。これらを覚える必要はありません。必要に応じて検索してください。

脚注
  1. JavaScript には、イベントリスナーの他に「イベントハンドラ」という用語もあります。これは addEventListener の仕様(DOM Level3)が勧告される前に使われていた構文 エレメント.onclick = function () {...} におけるコールバック関数を指します。厳密には、イベントリスナーのほうは関数そのものを指すのではなく、イベントと関数の “対応付け” を指すらしいのですが、MDN Web Docs でもイベントリスナーとイベンドハンドラの使い分けが曖昧だったりするので、いずれも「イベントに対応付けられた関数」と覚えておけばよいでしょう。 ↩︎

  2. 正確には、Event オブジェクトの中でも click イベントによって生じる MouseEvent オブジェクトが持つプロパティです。 ↩︎