Клавиатура: keyup, keydown, keypress

Здесь мы рассмотрим основные «клавиатурные» события и работу с ними.

Тестовый стенд

Для того, чтобы лучше понять, как работают события клавиатуры, можно использовать тестовый стенд.

Попробуйте различные варианты нажатия клавиш в текстовом поле.

Результат
script.js
style.css
index.html
kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle;

var lastTime = Date.now();

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

  var text = e.type +
    ' keyCode=' + e.keyCode +
    ' which=' + e.which +
    ' charCode=' + e.charCode +
    ' char=' + String.fromCharCode(e.keyCode || e.charCode) +
    (e.shiftKey ? ' +shift' : '') +
    (e.ctrlKey ? ' +ctrl' : '') +
    (e.altKey ? ' +alt' : '') +
    (e.metaKey ? ' +meta' : '') + "\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">

    Предотвратить действие по умолчанию для:
    <label>
      <input type="checkbox" name="keydownStop" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
    <label>
      <input type="checkbox" name="keypressStop" value="1"> keypress</label>&nbsp;&nbsp;&nbsp;
    <label>
      <input type="checkbox" name="keyupStop" value="1"> keyup</label>

    <p>
      Игнорировать:
      <label>
        <input type="checkbox" name="keydownIgnore" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
      <label>
        <input type="checkbox" name="keypressIgnore" value="1"> keypress</label>&nbsp;&nbsp;&nbsp;
      <label>
        <input type="checkbox" name="keyupIgnore" value="1"> keyup</label>
    </p>

    <p>Сфокусируйтесь на поле и нажмите какую-нибудь клавишу.</p>

    <input type="text" placeholder="Клавиши нажимать тут" id="kinput">

    <textarea id="area"></textarea>
    <input type="button" value="Очистить" onclick="area.value = ''" />
  </form>

  </form>
  <script src="script.js"></script>


</body>

</html>

По мере чтения, если возникнут вопросы – возвращайтесь к этому стенду.

События keydown и keyup

События keydown/keyup происходят при нажатии/отпускании клавиши и позволяют получить её скан-код в свойстве keyCode.

Скан-код клавиши одинаков в любой раскладке и в любом регистре. Например, клавиша z может означать символ "z", "Z" или "я", "Я" в русской раскладке, но её скан-код будет всегда одинаков: 90.

В действии:

<input onkeydown="this.nextSibling.innerHTML = event.keyCode"> <b></b>

Скан-коды

Для буквенно-цифровых клавиш есть очень простое правило: скан-код будет равен коду соответствующей заглавной английской буквы/цифры.

Например, при нажатии клавиши S (не важно, каков регистр и раскладка) её скан-код будет равен "S".charCodeAt(0).

Для других символов, в частности, знаков пунктуации, есть таблица кодов, которую можно взять, например, из статьи Джона Уолтера: JavaScript Madness: Keyboard Events, или же можно нажать на нужную клавишу на тестовом стенде и получить код.

Когда-то в этих кодах была масса кросс-браузерных несовместимостей. Сейчас всё проще – таблицы кодов в различных браузерах почти полностью совпадают. Но некоторые несовместимости, всё же, остались. Вы можете увидеть их в таблице ниже. Слева – клавиша с символом, а справа – скан-коды в различных браузерах.

Таблица несовместимостей:

Клавиша Firefox Остальные браузеры
; 59 186
= 107 187
- 109 188

Остальные коды одинаковы, код для нужного символа будет в тестовом стенде.

Событие keypress

Событие keypress возникает сразу после keydown, если нажата символьная клавиша, т.е. нажатие приводит к появлению символа.

Любые буквы, цифры генерируют keypress. Управляющие клавиши, такие как Ctrl, Shift, F1, F2… – keypress не генерируют.

Событие keypress позволяет получить код символа. В отличие от скан-кода, он специфичен именно для символа и различен для "z" и "я".

Код символа хранится в свойствах: charCode и which. Здесь скрывается целое «гнездо» кросс-браузерных несовместимостей, разбираться с которыми нет никакого смысла – запомнить сложно, а на практике нужна лишь одна «правильная» функция, позволяющая получить код везде.

Получение символа в keypress

Кросс-браузерная функция для получения символа из события keypress:

// event.type должен быть keypress
function getChar(event) {
  if (event.which == null) { // IE
    if (event.keyCode < 32) return null; // спец. символ
    return String.fromCharCode(event.keyCode)
  }

  if (event.which != 0 && event.charCode != 0) { // все кроме IE
    if (event.which < 32) return null; // спец. символ
    return String.fromCharCode(event.which); // остальные
  }

  return null; // спец. символ
}

Для общей информации – вот основные браузерные особенности, учтённые в getChar(event):

  1. Во всех браузерах, кроме IE, у события keypress есть свойство charCode, которое содержит код символа.
  2. Браузер IE для keypress не устанавливает charCode, а вместо этого он записывает код символа в keyCodekeydown/keyup там хранится скан-код).
  3. Также в функции выше используется проверка if(event.which!=0), а не более короткая if(event.which). Это не случайно! При event.which=null первое сравнение даст true, а второе – false.

В действии:

<input onkeypress="this.nextSibling.innerHTML = getChar(event)+''"><b></b>

Неправильный getChar

В сети вы можете найти другую функцию того же назначения:

function getChar(event) {
  return String.fromCharCode(event.keyCode || event.charCode);
}

Она работает неверно для многих специальных клавиш, потому что не фильтрует их. Например, она возвращает символ амперсанда "&", когда нажата клавиша „Стрелка Вверх“. Лучше использовать ту, что приведена выше.

Как и у других событий, связанных с пользовательским вводом, поддерживаются свойства shiftKey, ctrlKey, altKey и metaKey.

Они установлены в true, если нажаты клавиши-модификаторы – соответственно, Shift, Ctrl, Alt и Cmd для Mac.

Отмена пользовательского ввода

Появление символа можно предотвратить, если отменить действие браузера на keydown/keypress:

Попробуйте что-нибудь ввести в этих полях:
<input onkeydown="return false" type="text" size="30">
<input onkeypress="return false" type="text" size="30">

Попробуйте что-нибудь ввести в этих полях (не получится):

При тестировании на стенде вы можете заметить, что отмена действия браузера при keydown также предотвращает само событие keypress.

При keydown/keypress значение ещё старое

На момент срабатывания keydown/keypress клавиша ещё не обработана браузером.

Поэтому в обработчике значение input.value – старое, т.е. до ввода. Это можно увидеть в примере ниже. Вводите символы abcd.., а справа будет текущее input.value: abc..

А что, если мы хотим обработать input.value именно после ввода? Самое простое решение – использовать событие keyup, либо запланировать обработчик через setTimeout(..,0).

Отмена любых действий

Отменять можно не только символ, а любое действие клавиш.

Например:

  • При отмене Backspace – символ не удалится.
  • При отмене PageDown – страница не прокрутится.
  • При отмене Tab – курсор не перейдёт на следующее поле.

Конечно же, есть действия, которые в принципе нельзя отменить, в первую очередь – те, которые происходят на уровне операционной системы. Комбинация Alt+F4 инициирует закрытие браузера в Windows, что бы мы ни делали в JavaScript.

Демо: перевод символа в верхний регистр

В примере ниже действие браузера отменяется с помощью return false, а вместо него в input добавляется значение в верхнем регистре:

<input id="only-upper" type="text" size="2">
<script>
  document.getElementById('only-upper').onkeypress = function(e) {
    // спец. сочетание - не обрабатываем
    if (e.ctrlKey || e.altKey || e.metaKey) return;

    var char = getChar(e);

    if (!char) return; // спец. символ - не обрабатываем

    this.value = char.toUpperCase();

    return false;
  };
</script>

В действии:

Несовместимости

Некоторые несовместимости в порядке срабатывания клавиатурных событий (когда что) ещё существуют.

Стоит иметь в виду три основных категории клавиш, работа с которыми отличается.

Категория События Описание
Печатные клавиши S 1 , keydown
keypress
keyup
Нажатие вызывает keydown и keypress. Когда клавишу отпускают, срабатывает keyup.

Исключение – CapsLock под MacOS, с ним есть проблемы:

  • В Safari/Chrome/Opera: при включении только keydown, при отключении только keyup.
  • В Firefox: при включении и отключении только keydown.
Специальные клавиши Alt Esc keydown keyup Нажатие вызывает keydown. Когда клавишу отпускают, срабатывает keyup.

Некоторые браузеры могут дополнительно генерировать и keypress, например IE для Esc.

На практике это не доставляет проблем, так как для специальных клавиш мы всегда используем keydown/keyup.

Сочетания с печатной клавишей Alt+E
Ctrl+У
Cmd+1
keydown
keypress?
keyup

Браузеры под Windows – не генерируют keypress, браузеры под MacOS – генерируют.

Кроме того, если сочетание вызвало браузерное действие или диалог ("Сохранить файл", "Открыть" и т.п., ряд диалогов можно отменить при keydown), то может быть только keydown.

Общий вывод можно сделать такой:

  • Обычные символы работают везде корректно.
  • CapsLock под MacOS ведёт себя плохо, не стоит ставить на него обработчики вообще.
  • Для других специальных клавиш и сочетаний с ними следует использовать только keydown.

Автоповтор

При долгом нажатии клавиши возникает автоповтор. По стандарту, должны генерироваться многократные события keydown (+keypress), и вдобавок стоять свойство repeat=true у объекта события.

То есть поток событий должен быть такой:

keydown
keypress
keydown
keypress
..повторяется, пока клавиша не отжата...
keyup

Однако в реальности на это полагаться нельзя. На момент написания статьи, под Firefox(Linux) генерируется и keyup:

keydown
keypress
keyup
keydown
keypress
keyup
..повторяется, пока клавиша не отжата...
keyup

…А Chrome под MacOS не генерирует keypress. В общем, «зоопарк».

Полагаться можно только на keydown при каждом автонажатии и keyup по отпусканию клавиши.

Итого

Ряд рецептов по итогу этой главы:

  1. Для реализации горячих клавиш, включая сочетания – используем keydown. Скан-код будет в keyCode, почти все скан-коды кросс-браузерны, кроме нескольких пунктуационных, перечисленных в таблице выше.
  2. Если нужен именно символ – используем keypress. При этом функция getChar позволит получить символ и отфильтровать лишние срабатывания. Гарантированно получать символ можно только при нажатии обычных клавиш, если речь о сочетаниях с модификаторами, то keypress не всегда генерируется.
  3. Ловля CapsLock глючит под MacOS. Её можно организовать при помощи проверки navigator.userAgent и navigator.platform, а лучше вообще не трогать эту клавишу.

Распространённая ошибка – использовать события клавиатуры для работы с полями ввода в формах.

Это нежелательно. События клавиатуры предназначены именно для работы с клавиатурой. Да, их можно использовать для проверки ввода в <input>, но будут недочёты. Например, текст может быть вставлен мышкой, при помощи правого клика и меню, без единого нажатия клавиши. И как нам помогут события клавиатуры?

Некоторые мобильные устройства также не генерируют keypress/keydown, а сразу вставляют текст в поле. Обработать ввод на них при помощи клавиатурных событий нельзя.

Далее мы разберём события для элементов форм, которые позволяют работать с вводом в формы правильно.

Их можно использовать как отдельно от событий клавиатуры, так и вместе с ними.

Задачи

важность: 5

При помощи событий клавиатуры сделайте так, чтобы в поле можно было вводить только цифры. Пример ниже.

В поле должны нормально работать специальные клавиши Delete/Backspace и сочетания с Ctrl/Alt/Cmd.

P.S. Конечно, при помощи альтернативных способов ввода (например, вставки мышью), посетитель всё же может ввести что угодно.

Открыть песочницу для задачи.

Подсказка: выбор события

Нам нужно событие keypress, так как по скан-коду мы не отличим, например, клавишу '2' обычную и в верхнем регистре (символ '@').

Нужно отменять действие по умолчанию (т.е. ввод), если введена не цифра.

Решение

Нам нужно проверять символы при вводе, поэтому, будем использовать событие keypress.

Алгоритм такой: получаем символ и проверяем, является ли он цифрой. Если не является, то отменяем действие по умолчанию.

Кроме того, игнорируем специальные символы и нажатия со включенным Ctrl/Alt/Cmd.

Итак, вот решение:

input.onkeypress = function(e) {
  e = e || event;

  if (e.ctrlKey || e.altKey || e.metaKey) return;

  var chr = getChar(e);

  // с null надо осторожно в неравенствах,
  // т.к. например null >= '0' => true
  // на всякий случай лучше вынести проверку chr == null отдельно
  if (chr == null) return;

  if (chr < '0' || chr > '9') {
    return false;
  }
}

Открыть решение в песочнице.

важность: 3

Создайте функцию runOnKeys(func, code1, code2, ... code_n), которая запускает func при одновременном нажатии клавиш со скан-кодами code1, code2, …, code_n.

Например, код ниже выведет alert при одновременном нажатии клавиш "Q" и "W" (в любом регистре, в любой раскладке)

runOnKeys(
  function() { alert("Привет!") },
  "Q".charCodeAt(0),
  "W".charCodeAt(0)
);
Демо в новом окне

Ход решения

  • Функция runOnKeys – с переменным числом аргументов. Для их получения используйте arguments.
  • Используйте два обработчика: document.onkeydown и document.onkeyup. Первый отмечает нажатие клавиши в объекте pressed = {}, устанавливая pressed[keyCode] = true, а второй – удаляет это свойство. Если все клавиши с кодами из arguments нажаты – запускайте func.
  • Возникнет проблема с повторным нажатием сочетания клавиш после alert, решите её.
Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.