2022年9月21日

ブラウザイベント入門

イベントとは、何かが発生したという信号です。すべてのDOMノードはこうした信号を生成します(ただし、イベントはDOMに限定されません)。

最も有用なDOMイベントのリストを以下に示します。

マウスイベント

  • click – マウスが要素をクリックしたとき(タッチスクリーンデバイスではタップで発生します)。
  • contextmenu – マウスで要素を右クリックしたとき。
  • mouseover / mouseout – マウスカーソルが要素の上に来たとき/要素から離れたとき。
  • mousedown / mouseup – マウスボタンが要素の上で押されたとき/離されたとき。
  • mousemove – マウスが移動したとき。

キーボードイベント

  • keydown および keyup – キーボードのキーが押されたときと離されたとき。

フォーム要素イベント

  • submit – 訪問者が<form>を送信したとき。
  • focus – 訪問者が要素(例:<input>)にフォーカスしたとき。

ドキュメントイベント

  • DOMContentLoaded – HTMLが読み込まれて処理され、DOMが完全に構築されたとき。

CSSイベント

  • transitionend – CSSアニメーションが終了したとき。

他にも多くのイベントがあります。今後の章で、特定のイベントの詳細について説明します。

イベントハンドラ

イベントに反応するために、ハンドラ(イベントが発生した場合に実行される関数)を割り当てることができます。

ハンドラは、ユーザー操作が発生した場合にJavaScriptコードを実行する方法です。

ハンドラを割り当てる方法はいくつかあります。最も簡単な方法から見ていきましょう。

HTML属性

on<event>という名前の属性を使用して、HTMLでハンドラを設定できます。

たとえば、inputclickハンドラを割り当てるには、ここに示すようにonclickを使用できます。

<input value="Click me" onclick="alert('Click!')" type="button">

マウスをクリックすると、onclick内のコードが実行されます。

onclick内では、属性自体が二重引用符で囲まれているため、単一引用符を使用することに注意してください。属性内にあることを忘れて、このように二重引用符を内部で使用すると、onclick="alert("Click!")" 正しく動作しません。

HTML属性は多くのコードを記述するのに便利な場所ではないため、JavaScript関数を生成してそこで呼び出す方が良いでしょう。

クリックすると、関数countRabbits()が実行されます。

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">

ご存知のように、HTML属性名は大文字と小文字が区別されないため、ONCLICKonClickonCLICKと同じように機能します…しかし、通常は属性は小文字で記述されます:onclick

DOMプロパティ

DOMプロパティon<event>を使用してハンドラを割り当てることができます。

たとえば、elem.onclick

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>

HTML属性を使用してハンドラが割り当てられると、ブラウザはその属性を読み取り、属性の内容から新しい関数を生成し、それをDOMプロパティに書き込みます。

そのため、この方法は実際には前の方法と同じです。

これらの2つのコードスニペットは同じように機能します。

  1. HTMLのみ

    <input type="button" onclick="alert('Click!')" value="Button">
  2. HTML + JS

    <input type="button" id="button" value="Button">
    <script>
      button.onclick = function() {
        alert('Click!');
      };
    </script>

最初の例では、HTML属性を使用してbutton.onclickを初期化しますが、2番目の例ではスクリプトを使用します。これが唯一の違いです。

onclickプロパティは1つしかないため、複数のイベントハンドラを割り当てることはできません。

以下の例では、JavaScriptを使用してハンドラを追加すると、既存のハンドラが上書きされます。

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // overwrites the existing handler
    alert('After'); // only this will be shown
  };
</script>

ハンドラを削除するには、elem.onclick = nullを代入します。

要素へのアクセス:this

ハンドラ内のthisの値は、要素です。ハンドラが設定されている要素です。

以下のコードでは、buttonthis.innerHTMLを使用してその内容を表示します。

<button onclick="alert(this.innerHTML)">Click me</button>

起こりうる間違い

イベントを使い始める場合は、いくつかの微妙な点に注意してください。

既存の関数をハンドラとして設定できます。

function sayThanks() {
  alert('Thanks!');
}

elem.onclick = sayThanks;

しかし注意してください:関数はsayThanksとして割り当てる必要があり、sayThanks()ではありません。

// right
button.onclick = sayThanks;

// wrong
button.onclick = sayThanks();

括弧を追加すると、sayThanks()は関数呼び出しになります。そのため、最後の行は実際には関数の実行結果であるundefined(関数は何も返さないため)を取得し、それをonclickに割り当てます。これは機能しません。

…一方、マークアップでは括弧が必要です。

<input type="button" id="button" onclick="sayThanks()">

違いは簡単に説明できます。ブラウザが属性を読み取ると、属性の内容から本体を持つハンドラ関数を生成します。

そのため、マークアップはこのプロパティを生成します。

button.onclick = function() {
  sayThanks(); // <-- the attribute content goes here
};

ハンドラにはsetAttributeを使用しないでください。

このような呼び出しは機能しません。

// a click on <body> will generate errors,
// because attributes are always strings, function becomes a string
document.body.setAttribute('onclick', function() { alert(1) });

DOMプロパティでは大文字と小文字が区別されます。

DOMプロパティは大文字と小文字を区別するため、elem.ONCLICKではなくelem.onclickにハンドラを割り当てます。

addEventListener

前述のハンドラ割り当て方法の基本的な問題は、1つのイベントに複数のハンドラを割り当てることができないことです。

たとえば、コードの一部がクリック時にボタンを強調表示し、別の部分が同じクリック時にメッセージを表示したいとします。

そのためには2つのイベントハンドラを割り当てたいのですが、新しいDOMプロパティは既存のプロパティを上書きします。

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // replaces the previous handler

Web標準の開発者は、長い間それを理解しており、このような制約に縛られない特別なメソッドaddEventListenerremoveEventListenerを使用してハンドラを管理する代替方法を提案しました。

ハンドラを追加する構文

element.addEventListener(event, handler, [options]);
イベント
イベント名、例:"click"
ハンドラ
ハンドラ関数。
オプション
プロパティを持つ追加のオプションオブジェクト
  • oncetrueの場合、リスナーはトリガーされた後に自動的に削除されます。
  • capture:イベントを処理するフェーズ。後の章バブリングとキャプチャで説明します。歴史的な理由から、optionsfalse/trueでもかまいません。これは{capture: false/true}と同じです。
  • passivetrueの場合、ハンドラはpreventDefault()を呼び出しません。ブラウザのデフォルトアクションで後で説明します。

ハンドラを削除するには、removeEventListenerを使用します。

element.removeEventListener(event, handler, [options]);
削除するには同じ関数が必要です。

ハンドラを削除するには、割り当てられたものとまったく同じ関数を渡す必要があります。

これは機能しません。

elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));

removeEventListenerは別の関数(同じコードを持つが、それは問題ではなく、異なる関数オブジェクトである)を取得するため、ハンドラは削除されません。

正しい方法は次のとおりです。

function handler() {
  alert( 'Thanks!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

注意してください - 関数を変数に格納しないと、削除できません。addEventListenerで割り当てられたハンドラを「読み取る」方法はありません。

addEventListenerを複数回呼び出すと、次のように複数のハンドラを追加できます。

<input id="elem" type="button" value="Click me"/>

<script>
  function handler1() {
    alert('Thanks!');
  };

  function handler2() {
    alert('Thanks again!');
  }

  elem.onclick = () => alert("Hello");
  elem.addEventListener("click", handler1); // Thanks!
  elem.addEventListener("click", handler2); // Thanks again!
</script>

上記の例でわかるように、DOMプロパティとaddEventListenerの両方を使用してハンドラを設定できます。しかし、一般的にはこれらの方法のいずれか1つだけを使用します。

一部のイベントでは、ハンドラはaddEventListenerでのみ機能します。

DOMプロパティでは割り当てることができないイベントがあります。addEventListenerでのみ可能です。

たとえば、ドキュメントが読み込まれ、DOMが構築されたときにトリガーされるDOMContentLoadedイベントです。

// will never run
document.onDOMContentLoaded = function() {
  alert("DOM built");
};
// this way it works
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built");
});

そのため、addEventListenerの方が汎用性があります。ただし、このようなイベントは例外的なものです。

イベントオブジェクト

イベントを適切に処理するには、何が起こったかについて詳しく知りたいでしょう。「クリック」や「キーダウン」だけでなく、ポインタ座標は?どのキーが押されましたか?などです。

イベントが発生すると、ブラウザはイベントオブジェクトを作成し、詳細をそこに格納し、それを引数としてハンドラに渡します。

イベントオブジェクトからポインタ座標を取得する例を次に示します。

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // show event type, element and coordinates of the click
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

eventオブジェクトのいくつかのプロパティ

event.type
イベントの種類、ここでは"click"です。
event.currentTarget
イベントを処理した要素。ハンドラがアロー関数でない限り、またはそのthisが他のものにバインドされていない限り、thisとまったく同じです。その場合は、event.currentTargetから要素を取得できます。
event.clientX / event.clientY
ポインタイベントの場合、カーソルのウィンドウ相対座標。

さらに多くのプロパティがあります。それらの多くはイベントの種類に依存します。キーボードイベントには1つのプロパティセットがあり、ポインタイベントには別のプロパティセットがあります。さまざまなイベントの詳細に移行する際に、後でそれらを学習します。

イベントオブジェクトはHTMLハンドラでも利用できます。

HTMLでハンドラを割り当てると、次のようにeventオブジェクトを使用することもできます。

<input type="button" onclick="alert(event.type)" value="Event type">

ブラウザが属性を読み取ると、function(event) { alert(event.type) }のようなハンドラが作成されるため、可能です。つまり、その最初の引数は"event"と呼ばれ、本体は属性から取得されます。

オブジェクトハンドラ:handleEvent

addEventListenerを使用して、関数だけでなくオブジェクトをイベントハンドラとして割り当てることができます。イベントが発生すると、そのhandleEventメソッドが呼び出されます。

たとえば

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

ご覧のとおり、addEventListenerがハンドラとしてオブジェクトを受け取ると、イベントが発生した場合にobj.handleEvent(event)が呼び出されます。

次のようにカスタムクラスのオブジェクトを使用することもできます。

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "Mouse button pressed";
          break;
        case 'mouseup':
          elem.innerHTML += "...and released.";
          break;
      }
    }
  }

  let menu = new Menu();

  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

ここでは、同じオブジェクトが両方のイベントを処理します。addEventListener を使用して、リスンするイベントを明示的に設定する必要があることに注意してください。menu オブジェクトはここではmousedownmouseup のイベントのみを取得し、他の種類のイベントは取得しません。

handleEvent メソッドは、すべてを単独で行う必要はありません。代わりに、以下のように、イベント固有の他のメソッドを呼び出すことができます。

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

これで、イベントハンドラが明確に分離され、サポートが容易になる可能性があります。

概要

イベントハンドラの割り当て方法は3つあります。

  1. HTML属性: onclick="..."
  2. DOMプロパティ: elem.onclick = function
  3. メソッド: 追加するにはelem.addEventListener(event, handler[, phase])、削除するにはremoveEventListener

HTML属性は控えめに使用します。HTMLタグの中にあるJavaScriptは、少し奇妙で異質に見えるからです。また、大量のコードをそこに記述することもできません。

DOMプロパティを使用しても問題ありませんが、特定のイベントのハンドラを複数割り当てることはできません。多くの場合、その制限は差し迫ったものではありません。

最後の方法は最も柔軟性がありますが、記述するのも最も長くなります。これだけで動作するイベントはいくつかあり、たとえばtransitionendDOMContentLoaded (後述)があります。また、addEventListener はオブジェクトをイベントハンドラとしてサポートしています。その場合、イベントが発生するとhandleEventメソッドが呼び出されます。

ハンドラをどのように割り当てても、最初の引数としてイベントオブジェクトを受け取ります。そのオブジェクトには、何が起こったかについての詳細が含まれています。

イベント全般とさまざまな種類のイベントについては、次の章で詳しく説明します。

課題

重要度: 5

<div id="text"> をクリックしたときに消えるように、button にJavaScriptを追加します。

デモ

課題用のサンドボックスを開きます。

重要度: 5

クリックすると自身を非表示にするボタンを作成します。

ここでは、「要素自体」を参照するためにハンドラ内でthisを使用できます。

<input type="button" onclick="this.hidden=true" value="Click to hide">
重要度: 5

変数にボタンがあります。ボタンにはハンドラがありません。

次のコードの後、クリック時にどのハンドラが実行されますか?どの警告が表示されますか?

button.addEventListener("click", () => alert("1"));

button.removeEventListener("click", () => alert("1"));

button.onclick = () => alert(2);

答え: 12

最初のハンドラは、removeEventListener によって削除されていないためトリガーされます。ハンドラを削除するには、割り当てられた関数とまったく同じ関数を渡す必要があります。そして、コードでは同じように見えるが、別の関数である新しい関数が渡されます。

関数オブジェクトを削除するには、以下のように参照を保存する必要があります。

function handler() {
  alert(1);
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

ハンドラbutton.onclickは、addEventListenerとは独立して、さらに追加して動作します。

重要度: 5

クリックでフィールド全体にボールを移動させます。例

要件

  • ボールの中心が、クリック時にポインタの真下になるようにします(フィールドの端を越えずに可能な場合)。
  • CSSアニメーションは歓迎します。
  • ボールはフィールドの境界を越えてはいけません。
  • ページをスクロールしても、何も壊れてはいけません。

注意事項

  • コードは、異なるボールとフィールドのサイズでも動作し、固定値に縛られないようにする必要があります。
  • クリック座標には、event.clientX/event.clientYプロパティを使用します。

課題用のサンドボックスを開きます。

まず、ボールの位置決め方法を選択する必要があります。

ページのスクロールによってボールがフィールドから移動するため、position:fixed を使用することはできません。

そのため、position:absolute を使用し、位置決めを本当に堅牢にするために、field 自体を配置する必要があります。

すると、ボールはフィールドを基準にして配置されます。

#field {
  width: 200px;
  height: 150px;
  position: relative;
}

#ball {
  position: absolute;
  left: 0; /* relative to the closest positioned ancestor (field) */
  top: 0;
  transition: 1s all; /* CSS animation for left/top makes the ball fly */
}

次に、正しいball.style.left/topを割り当てる必要があります。これらには、フィールド相対座標が含まれるようになりました。

event.clientX/clientYがあります – クリックのウィンドウ相対座標。

クリックのフィールド相対left座標を取得するには、フィールドの左端とボーダー幅を減算できます。

let left = event.clientX - fieldCoords.left - field.clientLeft;

通常、ball.style.leftは「要素の左端」(ボール)を意味します。そのため、そのleftを割り当てると、ボールの端ではなく中心がマウスカーソルの下にきます。

中心にするには、ボールの幅の半分を左に、高さの半分を上に移動する必要があります。

したがって、最終的なleftは次のようになります。

let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;

垂直座標は同じロジックを使用して計算されます。

ball.offsetWidthにアクセスする時点で、ボールの幅/高さがわかっている必要があることに注意してください。HTMLまたはCSSで指定する必要があります。

サンドボックスで解答を開きます。

重要度: 5

クリックで開閉するメニューを作成します。

補足:ソースドキュメントのHTML/CSSを変更する必要があります。

課題用のサンドボックスを開きます。

HTML/CSS

最初にHTML/CSSを作成しましょう。

メニューはページ上のスタンドアロンのグラフィックコンポーネントであるため、単一のDOM要素に配置するのが最適です。

メニュー項目のリストは、ul/liリストとしてレイアウトできます。

例として構造を示します。

<div class="menu">
  <span class="title">Sweeties (click me)!</span>
  <ul>
    <li>Cake</li>
    <li>Donut</li>
    <li>Honey</li>
  </ul>
</div>

タイトルには<span>を使用します。なぜなら、<div>には暗黙的なdisplay:blockがあり、水平幅の100%を占めるためです。

<div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>

そのため、そこにonclickを設定すると、テキストの右側へのクリックもキャッチします。

<span>には暗黙的なdisplay: inlineがあるため、テキスト全体に合うのに必要なスペースだけを占めます。

<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>

メニューの切り替え

メニューを切り替えると、矢印が変わり、メニューリストが表示/非表示になります。

これらの変更はすべてCSSで完全に処理できます。JavaScriptでは、.openクラスを追加/削除することで、メニューの現在の状態にラベルを付けます。

このクラスがないと、メニューは閉じられます。

.menu ul {
  margin: 0;
  list-style: none;
  padding-left: 20px;
  display: none;
}

.menu .title::before {
  content: '▶ ';
  font-size: 80%;
  color: green;
}

…そして、.openクラスがあると、矢印が変わり、リストが表示されます。

.menu.open .title::before {
  content: '▼ ';
}

.menu.open ul {
  display: block;
}

サンドボックスで解答を開きます。

重要度: 5

メッセージのリストがあります。

JavaScriptを使用して、各メッセージの右上に閉じるボタンを追加します。

結果は次のようになります。

課題用のサンドボックスを開きます。

ボタンを追加するには、position:absolute(そしてペインをposition:relativeにする)またはfloat:rightのいずれかを使用できます。float:rightには、ボタンがテキストと重ならないという利点がありますが、position:absoluteの方が自由度が高くなります。そのため、どちらを使用するかはあなた次第です。

その後、各ペインのコードは次のようになります。

pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');

その後、<button>pane.firstChildになるので、次のようにハンドラを追加できます。

pane.firstChild.onclick = () => pane.remove();

サンドボックスで解答を開きます。

重要度: 4

「カルーセル」を作成します – 矢印をクリックしてスクロールできる画像のリボン。

後で、無限スクロール、動的読み込みなどを追加できます。

補足:この課題では、HTML/CSS構造がソリューションの90%を占めます。

課題用のサンドボックスを開きます。

画像リボンは、<img>画像のul/liリストとして表すことができます。

通常、このようなリボンは幅がありますが、固定サイズの<div>を周囲に配置して「切り取る」ため、リボンの一部のみが表示されます。

リストを水平方向に表示するには、<li>に正しいCSSプロパティ(例:display: inline-block)を適用する必要があります。

<img>についても、デフォルトではinlineであるため、displayを調整する必要があります。「レターテール」用にインライン要素の下に余分なスペースが予約されているため、display:blockを使用して削除できます。

スクロールを行うには、<ul>をシフトできます。方法はたくさんあります。たとえば、margin-leftを変更するか(パフォーマンスが良い)、transform: translateX()を使用します。

外部の<div>は幅が固定されているため、「余分な」画像は切り取られます。

カルーセル全体はページ上の自己完結型の「グラフィックコンポーネント」であるため、単一の<div class="carousel">にラップして、内部のものをスタイル設定するのが最適です。

サンドボックスで解答を開きます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関するご提案がある場合は、コメントする代わりに、GitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには<code>タグを使用し、数行の場合は<pre>タグで囲み、10行以上の場合はサンドボックス(plnkrjsbincodepen…)を使用してください。