2022年1月21日

ブラウザのデフォルト動作

多くのイベントは、ブラウザによって自動的に実行される特定の動作につながります。

例えば

  • リンクをクリックする – そのURLへのナビゲーションを開始します。
  • フォームの送信ボタンをクリックする – サーバーへの送信を開始します。
  • テキストの上にマウスボタンを押して移動する – テキストを選択します。

JavaScriptでイベントを処理する場合、対応するブラウザの動作を行わず、代わりに別の動作を実装したい場合があります。

ブラウザの動作を阻止する

ブラウザに動作をさせたくないことを伝える方法は2つあります。

  • 主な方法は、`event`オブジェクトを使用することです。`event.preventDefault()`メソッドがあります。
  • `on<event>`(`addEventListener`ではない)を使用してハンドラーを割り当てている場合、`false`を返すことでも同じように機能します。

このHTMLでは、リンクをクリックしてもナビゲーションにはつながりません。ブラウザは何もしません。

<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>

次の例では、このテクニックを使用してJavaScript対応のメニューを作成します。

ハンドラーから`false`を返すのは例外です

イベントハンドラーによって返される値は通常無視されます。

`on<event>`を使用して割り当てられたハンドラーからの`return false`だけが例外です。

その他の場合、`return`値は無視されます。特に、`true`を返す意味はありません。

例:メニュー

このようなサイトメニューを考えてみましょう。

<ul id="menu" class="menu">
  <li><a href="/html">HTML</a></li>
  <li><a href="/javascript">JavaScript</a></li>
  <li><a href="/css">CSS</a></li>
</ul>

いくつかのCSSを使用した外観です。

メニュー項目は、ボタン`<button>`ではなく、HTMLリンク`<a>`として実装されています。そうする理由はいくつかあります。例えば

  • 多くの人が「右クリック」–「新しいウィンドウで開く」を使用します。`<button>`または`<span>`を使用すると、機能しません。
  • 検索エンジンはインデックス作成中に`<a href="...">`リンクをたどります。

そのため、マークアップでは`<a>`を使用します。しかし通常、JavaScriptでクリックを処理することを意図しています。したがって、デフォルトのブラウザアクションを阻止する必要があります。

ここではこのようになります。

menu.onclick = function(event) {
  if (event.target.nodeName != 'A') return;

  let href = event.target.getAttribute('href');
  alert( href ); // ...can be loading from the server, UI generation etc

  return false; // prevent browser action (don't go to the URL)
};

`return false`を省略すると、コードの実行後にブラウザは「デフォルトアクション」(`href`のURLへのナビゲーション)を実行します。そして、ここでは自分でクリックを処理しているので、それは必要ありません。

ちなみに、ここでイベントデリゲーションを使用すると、メニューが非常に柔軟になります。入れ子になったリストを追加し、CSSを使用して「下にスライド」させることができます。

後続イベント

特定のイベントは互いに流れ込んでいきます。最初のイベントを阻止すると、2番目のイベントは発生しません。

例えば、`<input>`フィールドでの`mousedown`は、そのフィールドへのフォーカスと`focus`イベントにつながります。`mousedown`イベントを阻止すると、フォーカスは発生しません。

下の最初の`<input>`をクリックしてみてください – `focus`イベントが発生します。しかし、2番目をクリックすると、フォーカスは発生しません。

<input value="Focus works" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Click me">

これは、`mousedown`でブラウザのアクションがキャンセルされるためです。入力に別の方法で入力すれば、フォーカスはまだ可能です。例えば、Tabキーを使用して、最初の入力から2番目の入力に切り替えることができます。しかし、マウスクリックではもうできません。

「パッシブ」ハンドラーオプション

`addEventListener`のオプションの`passive: true`オプションは、ハンドラーが`preventDefault()`を呼び出さないことをブラウザに知らせます。

なぜそれが必要になるのでしょうか?

モバイルデバイスでの`touchmove`(ユーザーが指を画面上で動かしたとき)など、デフォルトでスクロールを引き起こすイベントがありますが、ハンドラーで`preventDefault()`を使用してそのスクロールを阻止できます。

そのため、ブラウザはそのようなイベントを検出すると、最初にすべてのハンドラーを処理してから、`preventDefault`がどこでも呼び出されていない場合、スクロールを続行できます。これにより、UIに不要な遅延と「ちらつき」が発生する可能性があります。

`passive: true`オプションは、ハンドラーがスクロールをキャンセルしないことをブラウザに伝えます。すると、ブラウザは最大限にスムーズなエクスペリエンスを提供するためにすぐにスクロールし、イベントは処理されます。

一部のブラウザ(Firefox、Chrome)では、`touchstart`と`touchmove`イベントでは、`passive`はデフォルトで`true`です。

event.defaultPrevented

`event.defaultPrevented`プロパティは、デフォルトアクションが阻止された場合は`true`、そうでない場合は`false`になります。

それには興味深いユースケースがあります。

チャプターバブリングとキャプチャで、`event.stopPropagation()`について、そしてバブリングを停止することがなぜ悪いのかについて説明したことを覚えていますか?

他のイベントハンドラーにイベントが処理されたことを知らせるために、代わりに`event.defaultPrevented`を使用できる場合があります。

実用的な例を見てみましょう。

デフォルトでは、ブラウザは`contextmenu`イベント(右マウスクリック)で標準オプションを含むコンテキストメニューを表示します。これを阻止し、次のように独自のメニューを表示できます。

<button>Right-click shows browser context menu</button>

<button oncontextmenu="alert('Draw our menu'); return false">
  Right-click shows our context menu
</button>

今度は、そのコンテキストメニューに加えて、ドキュメント全体にわたるコンテキストメニューを実装したいと思います。

右クリックすると、最も近いコンテキストメニューが表示されるはずです。

<p>Right-click here for the document context menu</p>
<button id="elem">Right-click here for the button context menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

`elem`をクリックすると、ボタンレベルと(イベントはバブルアップする)ドキュメントレベルのメニューの2つのメニューが表示されるという問題があります。

どのように修正すればよいでしょうか?解決策の1つは、「ボタンハンドラーで右クリックを処理するときは、そのバブリングを停止する」と考えて`event.stopPropagation()`を使用することです。

<p>Right-click for the document menu</p>
<button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    event.stopPropagation();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

これで、ボタンレベルのメニューは意図したとおりに機能します。しかし、その代償は大きいです。統計などを収集するカウンターなど、外部のコードに対して右クリックに関する情報を永久にアクセスできなくします。それは非常に賢明ではありません。

別の解決策は、ドキュメントハンドラーでデフォルトアクションが阻止されたかどうかを確認することです。そうであれば、イベントは処理され、それに反応する必要はありません。

<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    if (event.defaultPrevented) return;

    event.preventDefault();
    alert("Document context menu");
  };
</script>

これで、すべてが正しく機能します。入れ子になった要素があり、それぞれに独自のコンテキストメニューがある場合も機能します。各`contextmenu`ハンドラーで`event.defaultPrevented`を確認してください。

event.stopPropagation()とevent.preventDefault()

明らかなように、`event.stopPropagation()`と`event.preventDefault()`(`return false`とも呼ばれる)は、まったく異なるものです。それらは互いに関連していません。

入れ子になったコンテキストメニューのアーキテクチャ

入れ子になったコンテキストメニューを実装する他の方法もあります。その1つは、`document.oncontextmenu`のハンドラーと、他のハンドラーを保存できるメソッドを持つ単一のグローバルオブジェクトを持つことです。

オブジェクトは右クリックをキャッチし、保存されているハンドラーを調べて適切なハンドラーを実行します。

しかし、コンテキストメニューが必要なコードは、独自の`contextmenu`ハンドラーではなく、そのオブジェクトについて知っており、その助けを使用する必要があります。

まとめ

デフォルトのブラウザアクションはたくさんあります。

  • `mousedown` – 選択を開始します(マウスを動かして選択します)。
  • `<input type="checkbox">`をクリックする – `input`をチェック/アンチェックします。
  • `submit` – `<input type="submit">`をクリックするか、フォームフィールド内でEnterキーを押すと、このイベントが発生し、ブラウザはその後にフォームを送信します。
  • `keydown` – キーを押すと、フィールドに文字を追加したり、他のアクションを実行したりすることがあります。
  • `contextmenu` – 右クリックでイベントが発生し、ブラウザのコンテキストメニューが表示されます。
  • …もっとあります…

イベントをJavaScriptだけで処理したい場合は、すべてのデフォルトアクションを阻止できます。

デフォルトアクションを阻止するには、`event.preventDefault()`または`return false`を使用します。2番目の方法は、`on<event>`で割り当てられたハンドラーでのみ機能します。

`addEventListener`の`passive: true`オプションは、アクションが阻止されないことをブラウザに伝えます。これは、`touchstart`や`touchmove`などの一部のモバイルイベントで、スクロールする前にすべてのハンドラーの完了を待たないようブラウザに指示するのに役立ちます。

デフォルトアクションが阻止された場合、`event.defaultPrevented`の値は`true`になり、そうでない場合は`false`になります。

セマンティックを維持し、乱用しない

技術的には、デフォルトアクションを阻止してJavaScriptを追加することで、任意の要素の動作をカスタマイズできます。例えば、リンク`<a>`をボタンのように動作させ、ボタン`<button>`をリンクのように動作させる(別のURLなどにリダイレクトする)ことができます。

しかし、一般的にHTML要素のセマンティックな意味を維持する必要があります。例えば、`<a>`はボタンではなく、ナビゲーションを実行する必要があります。

「単に良いこと」であることに加えて、アクセシビリティの点でHTMLを改善します。

また、`<a>`の例を考えると、ブラウザではそのようなリンクを新しいウィンドウで開くことができます(右クリックやその他の方法で)。そして、人々はそれを好みます。しかし、JavaScriptを使用してボタンをリンクのように動作させ、CSSを使用してリンクのように見せる場合でも、`<a>`固有のブラウザ機能は機能しません。

課題

重要度:3

以下のコードで`return false`がまったく機能しないのはなぜですか?

<script>
  function handler() {
    alert( "..." );
    return false;
  }
</script>

<a href="https://w3.org" onclick="handler()">the browser will go to w3.org</a>

ブラウザはクリック時にURLに従いますが、それは望んでいません。

修正方法

ブラウザは`onclick`などの`on*`属性を読み取ると、そのコンテンツからハンドラーを作成します。

`onclick="handler()"`の場合、関数は次のようになります。

function(event) {
  handler() // the content of onclick
}

これで、`handler()`によって返される値は使用されず、結果に影響を与えないことがわかります。

修正は簡単です。

<script>
  function handler() {
    alert("...");
    return false;
  }
</script>

<a href="https://w3.org" onclick="return handler()">w3.org</a>

また、次のように`event.preventDefault()`を使用することもできます。

<script>
  function handler(event) {
    alert("...");
    event.preventDefault();
  }
</script>

<a href="https://w3.org" onclick="handler(event)">w3.org</a>
重要度: 5

id="contents" の要素内にあるすべてのリンクに対し、本当に離れても良いかユーザーに確認を求めます。そして、離れない場合は遷移させません。

このように

詳細

  • 要素内のHTMLは動的にいつでも読み込まれたり再生成されたりする可能性があるため、すべてのリンクを見つけてハンドラーを付けることはできません。イベントデリゲーションを使用します。
  • コンテンツにはネストされたタグが含まれる場合があります。リンク内にも、<a href=".."><i>...</i></a> のように。

このタスクのためにサンドボックスを開きます。

これはイベントデリゲーションパターンの優れた使用方法です。

現実の世界では、確認の代わりに、訪問者がどこで離れたかの情報を保存する「ログ」リクエストをサーバーに送信できます。または、許可されている場合は、コンテンツを読み込んでページ内に直接表示することもできます。

必要なのは、contents.onclick をキャッチし、confirm を使用してユーザーに確認を求めることです。URLにはlink.href の代わりにlink.getAttribute('href') を使用することをお勧めします。詳細はソリューションを参照してください。

ソリューションをサンドボックスで開きます。

重要度: 5

サムネイルをクリックするとメイン画像が変わる画像ギャラリーを作成します。

このように

追伸:イベントデリゲーションを使用してください。

このタスクのためにサンドボックスを開きます。

ソリューションは、ハンドラーをコンテナに割り当て、クリックを追跡することです。<a> リンクをクリックした場合は、サムネイルのhref#largeImgsrc に変更します。

ソリューションをサンドボックスで開きます。

チュートリアルマップ

コメント

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