2022年4月25日

キーボード: keydownとkeyup

キーボードに入る前に、現代のデバイスには「何かを入力する」ための他の方法があることに注意してください。たとえば、人々は音声認識(特にモバイルデバイスで)や、マウスによるコピー/ペーストを使用します。

したがって、<input>フィールドへの入力を追跡したい場合、キーボードイベントだけでは十分ではありません。<input>フィールドの変更をあらゆる手段で追跡するためのinputという名前の別のイベントがあります。また、それはそのようなタスクにとってより良い選択肢かもしれません。それについては、後の章イベント: change, input, cut, copy, pasteで説明します。

キーボードイベントは、キーボードアクション(仮想キーボードもカウントします)を処理したい場合に使用する必要があります。たとえば、矢印キーやホットキー(キーの組み合わせを含む)に反応する場合です。

テストスタンド

キーボードイベントをよりよく理解するために、以下のテストスタンドを使用できます。

テキストフィールドで異なるキーの組み合わせを試してください。

結果
script.js
style.css
index.html
kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle;

let lastTime = Date.now();

function handle(e) {
  if (form.elements[e.type + 'Ignore'].checked) return;

  area.scrollTop = 1e6;

  let text = e.type +
    ' key=' + e.key +
    ' code=' + e.code +
    (e.shiftKey ? ' shiftKey' : '') +
    (e.ctrlKey ? ' ctrlKey' : '') +
    (e.altKey ? ' altKey' : '') +
    (e.metaKey ? ' metaKey' : '') +
    (e.repeat ? ' (repeat)' : '') +
    "\n";

  if (area.value && Date.now() - lastTime > 250) {
    area.value += new Array(81).join('-') + '\n';
  }
  lastTime = Date.now();

  area.value += text;

  if (form.elements[e.type + 'Stop'].checked) {
    e.preventDefault();
  }
}
#kinput {
  font-size: 150%;
  box-sizing: border-box;
  width: 95%;
}

#area {
  width: 95%;
  box-sizing: border-box;
  height: 250px;
  border: 1px solid black;
  display: block;
}

form label {
  display: inline;
  white-space: nowrap;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <form id="form" onsubmit="return false">

    Prevent default for:
    <label>
      <input type="checkbox" name="keydownStop" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
    <label>
      <input type="checkbox" name="keyupStop" value="1"> keyup</label>

    <p>
      Ignore:
      <label>
        <input type="checkbox" name="keydownIgnore" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
      <label>
        <input type="checkbox" name="keyupIgnore" value="1"> keyup</label>
    </p>

    <p>Focus on the input field and press a key.</p>

    <input type="text" placeholder="Press keys here" id="kinput">

    <textarea id="area" readonly></textarea>
    <input type="button" value="Clear" onclick="area.value = ''" />
  </form>
  <script src="script.js"></script>


</body>
</html>

keydownとkeyup

keydownイベントはキーが押されたときに発生し、keyupイベントはキーが離されたときに発生します。

event.codeとevent.key

イベントオブジェクトのkeyプロパティを使用すると文字を取得でき、イベントオブジェクトのcodeプロパティを使用すると「物理キーコード」を取得できます。

たとえば、同じキーZShiftキーと組み合わせて、または単独で押すことができます。これにより、小文字のzと大文字のZという2つの異なる文字が得られます。

event.keyは正確に文字であり、異なる値になります。しかし、event.codeは同じ値になります。

キー event.key event.code
Z z(小文字) KeyZ
Shift+Z Z(大文字) KeyZ

ユーザーが異なる言語を使用している場合、別の言語に切り替えると、"Z"の代わりにまったく異なる文字になります。それがevent.keyの値になりますが、event.codeは常に同じ"KeyZ"になります。

“KeyZ”とその他のキーコード

すべてのキーには、キーボード上の場所によって異なるコードがあります。キーコードはUI Events code specificationに記述されています。

たとえば

  • 文字キーには"Key<文字>"というコードがあります。例: "KeyA", "KeyB"など
  • 数字キーには"Digit<数字>"というコードがあります。例: "Digit0", "Digit1"など
  • 特殊キーは名前でコード化されています。例: "Enter", "Backspace", "Tab"など

いくつかの普及したキーボードレイアウトがあり、仕様ではそれらのそれぞれにキーコードが提供されています。

その他のコードについては、仕様の英数字セクションを読むか、上記のテストスタンドでキーを押してください。

大文字と小文字が重要: "KeyZ", "keyZ"ではない

明白に見えますが、人々はまだミスをします。

タイプミスを避けてください。keyZではなく、KeyZです。event.code=="keyZ"のようなチェックは機能しません。"Key"の最初の文字は大文字である必要があります。

キーが文字を生成しない場合はどうなりますか?たとえば、ShiftF1などです。これらのキーの場合、event.keyevent.codeとほぼ同じです。

キー event.key event.code
F1 F1 F1
バックスペース バックスペース バックスペース
Shift Shift ShiftRightまたはShiftLeft

event.codeは、どのキーが押されたかを正確に指定することに注意してください。たとえば、ほとんどのキーボードには、左側と右側に2つのShiftキーがあります。event.codeは、どのキーが押されたかを正確に示し、event.keyはキーの「意味」を担当します: それが何であるか("Shift")。

たとえば、ホットキーを処理したいとします: Ctrl+Z (またはMacの場合はCmd+Z)。ほとんどのテキストエディタは、それに「元に戻す」アクションをフックします。keydownでリスナーを設定し、どのキーが押されているかを確認できます。

ここにはジレンマがあります。このようなリスナーでは、event.keyevent.codeのどちらの値を確認する必要がありますか?

一方、event.keyの値は文字であり、言語によって変化します。訪問者のOSに複数の言語があり、それらを切り替える場合、同じキーで異なる文字が生成されます。したがって、常に同じであるevent.codeを確認するのが理にかなっています。

このように。

document.addEventListener('keydown', function(event) {
  if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
    alert('Undo!')
  }
});

他方、event.codeには問題があります。異なるキーボードレイアウトでは、同じキーで異なる文字になる場合があります。

たとえば、ここに米国レイアウト("QWERTY")とその下のドイツレイアウト("QWERTZ")があります(Wikipediaより)

同じキーの場合、米国レイアウトには"Z"があり、ドイツレイアウトには"Y"があります(文字が入れ替わっています)。

文字通り、ドイツレイアウトを使用している人がYを押すと、event.codeKeyZと等しくなります。

コードでevent.code == 'KeyZ'をチェックすると、ドイツレイアウトを使用している人がYを押したときに、そのようなテストに合格します。

それは非常に奇妙に聞こえますが、その通りです。仕様では、このような動作を明示的に述べています。

そのため、event.codeは、予期しないレイアウトに対して誤った文字と一致する場合があります。異なるレイアウトの同じ文字が異なる物理キーにマップされ、異なるコードになる可能性があります。幸いなことに、それはいくつかのコード(例: keyA, keyQ, keyZ (これまで見てきたように))でのみ発生し、Shiftなどの特殊キーでは発生しません。 仕様でリストを見つけることができます。

レイアウトに依存する文字を確実に追跡するには、event.keyの方が良い方法かもしれません。

一方、event.codeには、物理的なキーの位置に常に同じままになるという利点があります。したがって、それに依存するホットキーは、言語を切り替えた場合でも正常に機能します。

レイアウトに依存するキーを処理したいですか? その場合は、event.keyを使用するのが良いでしょう。

それとも、言語を切り替えた後でもホットキーを機能させたいですか? その場合は、event.codeの方が良いかもしれません。

自動反復

キーが十分に長い時間押されている場合、キーは「自動反復」を開始します: keydownが何度もトリガーされ、離されたときに最終的にkeyupが発生します。したがって、多くのkeydownと1つのkeyupを持つのが正常です。

自動反復によってトリガーされたイベントの場合、イベントオブジェクトにはevent.repeatプロパティがtrueに設定されています。

デフォルトアクション

キーボードで開始できることがたくさんあるため、デフォルトアクションは異なります。

たとえば

  • 文字が画面に表示されます(最も明らかな結果)。
  • 文字が削除されます(Deleteキー)。
  • ページがスクロールされます(PageDownキー)。
  • ブラウザが「ページを保存」ダイアログを開きます(Ctrl+S)。
  • …など。

keydownでデフォルトアクションを防止すると、OSベースの特殊キーを除いて、ほとんどのアクションをキャンセルできます。たとえば、Windowsでは、Alt+F4で現在のブラウザウィンドウが閉じられます。そして、JavaScriptでデフォルトアクションを防止しても、それを止めることはできません。

たとえば、以下の<input>は電話番号を想定しているため、数字、+()、または-以外のキーを受け入れません。

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') || ['+','(',')','-'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

ここでのonkeydownハンドラーは、checkPhoneKeyを使用して押されたキーをチェックします。それが有効(0..9、または+-()のいずれか)の場合、trueを返し、それ以外の場合はfalseを返します。

上記のように、DOMプロパティまたは属性を使用して割り当てられたイベントハンドラーから返されたfalse値は、デフォルトのアクションを防止するため、テストに合格しないキーの場合、<input>には何も表示されないことがわかります。(返されたtrue値は何も影響しません。falseを返すことのみが重要です)

Backspace, , などの特殊キーが入力で機能しないことに注意してください。これは、厳密なフィルターcheckPhoneKeyの副作用です。これらのキーは、falseを返すようにします。

, の矢印キーと、Delete, Backspaceを許可して、フィルターを少し緩和しましょう。

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') ||
    ['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

これで、矢印と削除が正常に機能します。

キーフィルターがあっても、マウスと右クリック+貼り付けを使用して何でも入力できます。モバイルデバイスには、値を入力するための他の手段があります。したがって、フィルターは100%信頼できるとは限りません。

別の方法は、oninputイベントを追跡することです。これは、変更にトリガーされます。ここで、新しいinput.valueをチェックし、無効な場合は<input>を変更/強調表示できます。または、両方のイベントハンドラーを一緒に使用することもできます。

レガシー

過去には、keypressイベントと、イベントオブジェクトのkeyCode, charCode, whichプロパティがありました。

それらを扱うときに多くのブラウザの非互換性があったため、仕様の開発者は、それらすべてを廃止し、新しい最新のイベント(この章で上記に説明)を作成する以外に方法がありませんでした。古いコードは、ブラウザがサポートを継続しているため、まだ機能しますが、それらをもう使用する必要はまったくありません。

モバイルキーボード

IME (Input-Method Editor)として正式に知られている仮想/モバイルキーボードを使用する場合、W3C標準では、KeyboardEventのe.keyCode229で、e.key"Unidentified"である必要があると述べています。

これらのキーボードの中には、矢印キーやバックスペースキーなどの特定のキーを押したときに、e.keye.codee.keyCode に正しい値が設定されるものもありますが、保証はありません。そのため、キーボードのロジックがモバイルデバイスで常に動作するとは限りません。

まとめ

キーを押すと、シンボルキーでも、ShiftCtrlなどの特殊キーでも、常にキーボードイベントが発生します。唯一の例外は、ラップトップのキーボードにあることがあるFnキーです。これはOSよりも低いレベルで実装されていることが多いため、キーボードイベントは発生しません。

キーボードイベント

  • keydown – キーを押したとき (キーを長く押すと自動的に繰り返される)、
  • keyup – キーを離したとき。

主なキーボードイベントプロパティ

  • code – 「キーコード」("KeyA""ArrowLeft" など)。キーボード上のキーの物理的な位置に固有。
  • key – 文字 ("A""a" など)。Escなどの非文字キーの場合、通常は code と同じ値を持ちます。

以前は、キーボードイベントがフォームフィールドでのユーザー入力を追跡するために使用されることがありました。しかし、入力はさまざまなソースから来る可能性があるため、信頼性がありません。あらゆる入力 (コピー&ペーストや音声認識を含む) を処理するために、input イベントと change イベントがあります (この章の後半にある「イベント: change, input, cut, copy, paste」で説明します)。

キーボードイベントは、本当にキーボードが必要な場合に使用する必要があります。たとえば、ホットキーや特殊キーに反応する場合などです。

課題

重要度: 5

runOnKeys(func, code1, code2, ... code_n) という関数を作成してください。この関数は、コード code1, code2, …, code_n のキーが同時に押されたときに func を実行します。

たとえば、以下のコードは "Q""W" が同時に押されたときに (どの言語でも、CapsLockの有無にかかわらず) alert を表示します。

runOnKeys(
  () => alert("Hello!"),
  "KeyQ",
  "KeyW"
);

新しいウィンドウでデモを見る

document.onkeydowndocument.onkeyup の2つのハンドラーを使用する必要があります。

現在押されているキーを保持するために、セット pressed = new Set() を作成しましょう。

最初のハンドラーはセットに追加し、2番目のハンドラーはセットから削除します。keydown のたびに、十分なキーが押されているかどうかを確認し、そうであれば関数を実行します。

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

チュートリアルマップ

コメント

コメントする前にこちらをお読みください…
  • 改善点があれば、コメントではなく、GitHub issue を提出するか、プルリクエストを送ってください。
  • 記事の内容が理解できない場合は、具体的に説明してください。
  • 数語のコードを挿入するには <code> タグを使用し、複数行の場合は <pre> タグで囲み、10行を超える場合はサンドボックス (plnkr, jsbin, codepen…) を使用してください。