2021年12月15日

ブラウザイベントの紹介

イベント は何かが起きたと言う信号です。すべての DOM ノードはこのような信号を生成します(ただし、イベントは DOM に限ったものではありません)。

ここでは最も有用なDOMイベントのリストを見てみましょう。:

マウスイベント:

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

フォーム要素イベント:

  • submit – 訪問者が <form> をサブミットしたとき。
  • focus – 訪問者が要素にフォーカスしたとき。e.g. <input>

キーボードイベント:

  • keydownkeyup – 訪問者がボタンを押したり離したとき。

ドキュメントイベント

  • DOMContentLoaded – HTMLがロードされ処理されたとき、DOM は完全に構築済みです。

CSS イベント:

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

他にも多くのイベントがあります。次のチャプターで特定のイベントについてより詳細を見ていきます。

イベントハンドラ

イベントに反応するために、ハンドラ – イベント発生時に実行する関数 – を割り当てることができます。

ハンドラは、ユーザのアクション時に JavaScript コードを実行する方法です。

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

HTML 属性

ハンドラは on<event> と言う名前の属性で、 HTML 上で設定できます。

例えば、input に対して click ハンドラを割り当てるためには、このように 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 プロパティに書きこみます。

したがって、この方法は実際には前の方法と同じです。

ハンドラは常に DOM プロパティにあります: HTML属性は単にその初期化の方法の1つにすぎません。

これらの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>

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

下の例では、JavaScript でのハンドラの追加は、既存のハンドラを上書きします:

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // 既存のハンドラを上書きします
    alert('After'); // これだけが表示されます
  };
</script>

ちなみに、既存の関数を直接ハンドラとして割り当てることもできます:

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

elem.onclick = sayThanks;

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

要素 this にアクセスする

ハンドラの中の this の値はその要素です。

下のコードでは、buttonthis.innerHTML で自身の中身を表示します:

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

ありそうなミス

イベントを使う場合は、微妙な点に注意してください。

関数は sayThanks() ではなく、sayThanks で割り当てる必要があります。

// 正しい
button.onclick = sayThanks;

// 誤り
button.onclick = sayThanks();

もしカッコをつけると、sayThanks() 関数の実行 結果 になるので、最後の行の onclickundefined (関数が何も返さない)になります。それは動作しません。

…しかしマークアップでは、カッコは必要です:

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

この違いは簡単に説明出来ます。ブラウザが属性を読みとると、その内容から本体を含むハンドラ関数が作成されます。

したがって、最後の例は次と同じです:

button.onclick = function() {
  sayThanks(); // ここが属性の中身
};

文字列ではなく関数を使用します。

割り当て elem.onclick = "alert(1)" も動作します。これは互換性のために動作しますが、強く推奨されません。

ハンドラに対して、setAttribute は使わないでください。

このような呼び出しは動作しません:

// <body> のクリックはエラーになります,
// なぜなら、属性は常に文字列であり、関数は文字列になります
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); } // 前のハンドラを上書きします

Web標準の開発者はずっと前に理解しており、特別なメソッド addEventListenerremoveEventListener を使うことで、ハンドラを管理する別の方法を提案しました。これらはこのような問題から解放されています。

ハンドラを追加する構文は次のようになります:

element.addEventListener(event, handler[, phase]);
event
イベント名, e.g. "click".
handler
ハンドラ関数.
phase
オプションの引数で、ハンドラが動作する “フェーズ” です。後ほど説明します。通常は使いません。

ハンドラを削除する場合は removeEventListener を使います:

// addEventListener とまったく同じ引数です
element.removeEventListener(event, handler[, phase]);
削除は同じ関数が必要です

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

これは動作しません:

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 両方を使ってハンドラを設定することができます。しかし、一般的にどちらかの方法を使います。

いくつかのイベントでは、ハンドラは addEventListenerでのみ動作します

DOMプロパティ経由では割り当てることができないイベントが存在します。addEventListener を使用しなければなりません。

例えば、transitionend (CSS アニメーションの終了) イベントなどです。

下のコードを試してみてください。ほとんどのブラウザでは2つ目のハンドラのみ動作し、1つ目は動作しません:

<style>
  input {
    transition: width 1s;
    width: 100px;
  }

  .wide {
    width: 300px;
  }
</style>

<input type="button" id="elem" onclick="this.classList.toggle('wide')" value="Click me">

<script>
  elem.ontransitionend = function() {
    alert("DOM property"); // 動作しません
  };

  elem.addEventListener("transitionend", function() {
    alert("addEventListener"); // アニメーションが終わったときに表示されます
  });
</script>

イベントオブジェクト

イベントを適切に処理するためには、私たちは何が起こったのかをもっと知りたいです。単に “click” や “keypress” だけではなく、ポインタの座標はなにか?どのキーが押されたのか?などです。

イベントが起こったとき、ブラウザは イベントオブジェクト を作り、そこに詳細を入れ、ハンドラの引数として渡します。

イベントオブジェクトからマウス座標を取得例です:

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

<script>
  elem.onclick = function(event) {
    // イベントタイプ、要素、クリック座標を表示
    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
マウスイベントに対するカーソルのウィンドウ相対座標です。

他にもプロパティがあります。それらはイベントのタイプによって異なりますので、詳細については別のイベントを扱う時にそれらを学びます。

イベントオブジェクトもまた 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>
  elem.addEventListener('click', {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  });
</script>

言い換えると、addEventListener がハンドラとしてオブジェクトを受け取ると、イベント時に object.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 プロパティは使うのは問題ありませんが、特定のイベントに対して1つ以上のハンドラを割り当てることができません。多くの場合、その制限は切実ではありません。

最後の方法は最も柔軟ですが、記述が最も長くなります。これでしか動作しないイベントがいくつかあります, 例えば transtionendDOMContentLoaded です。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.onclickaddEventListener に加えて、独立して動作します。

重要性: 5

クリックでフィールド上のボールを移動させてください。このように:

要件:

  • ボールの中心は、クリック時のポインタの下に正確に来る必要があります(可能な場合はフィールドの端を超えないでください)
  • CSS アニメーションは歓迎します
  • ボールはフィールドの境界を超えてはいけません
  • ページがスクロールしても壊れません

補足:

  • コードは、任意の固定しに縛られるのではなく、異なるボールやフィールドサイズでも動作する必要があります。
  • クリック座標のために event.clientX/event.clientY プロパティを使ってください。

タスクのためのサンドボックスを開く

まず、ボールを配置するメソッドを選ぶ必要があります。

ページをスクロールすると、フィールドからボールを移動させるため、position:fixed を使うことは出来ません。

なので、position:absolute を使うべきであり、配置を本当に堅実にするために、field 自体を位置決めしてください。

次に、ボールはフィールに相対的に配置されます:

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

#ball {
  position: absolute;
  left: 0; /* 最も近い配置された祖先(field)への相対 */
  top: 0;
  transition: 1s all; /* ボールを飛ばずための、左/上 に対するCSS アニメーション */
}

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

これはその図です:

私たちは event.clientX/clientY – クリックのウィンドウに相対的な座標 – を持っています。

クリック時のフィールドに相対的な left 座標を取得するには、フィールドの左端とボーダーの幅を引きます。:

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

通常、 ball.style.position.left は “要素(ボール)の左端” を意味します。なので、その left を割り当てると、ボールの端がマウスカーソルの下に来ることになります。

私たちは、それを中心にするために、ボールの半分の幅を左へ、半分の高さを上へ移動させる必要があります。

なので、最終的な left は次のようになります:

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

縦の座標は同じロジックを使って計算します。

ボールの幅/高さは、ball.offsetWidth にアクセスしたときに知っていなければならないことに注意してください。HTMLまたはCSSで指定する必要があります。

サンドボックスで解答を開く

重要性: 5

クリック時に 開く/閉じる メニューを作成してください:

P.S. ソースドキュメントの 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>

<div> は暗黙で display:block を持っており、水平幅の 100% を占めるため、タイトルには <span> を使います。

このようになります:

<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 の追加/削除によって、現在のメニューの状態を分類する必要があります。

.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

“カルーセル” – 矢印をクリックすることでスクロールできる画像のリボン – を作成してください。

あとで、そこへより多くの機能を追加できます: 無限スクロール、動的ローディングなど。

P.S. このタスクでは、HTML/CSS 構造が解決策の 90% です。

タスクのためのサンドボックスを開く

イメージのリボンは、イメージ <img>ul/li リストとして表現できます。

通常、このようなリボンは幅広ですが、リボンの一部のみが見えるよう、固定サイズの <div> を置いて、それを “切り取る” ようにします:

リストを水平に表示するために、display: inline-block のような <li> に対して正しい CSS プロパティを適用する必要があります。

<img> では、デフォルトでは inline なので display の調整も必要です。“文字の終わり” のための inline 要素の下に余分なスペースが確保されているので、display:block を使ってそれを削除します。

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

外部の <div> は固定幅なので、“余分な” イメージはカットされます。

カルーセル全体は、ページ上で、自身で構成されている “グラフィカルコンポーネント” なので、単一の <div class="carousel"> でまとめ、その中でスタイルするのが良いでしょう。

サンドボックスで解答を開く

チュートリアルマップ