События клавиатуры: key и code
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Клавиатура сообщает сразу две вещи: key — какой символ получился, code — какую физическую клавишу придавил палец. Из-за путаницы между ними горячие клавиши молча ломаются, стоит пользователю переключиться на русскую раскладку.
Каждое нажатие даёт два события: keydown — клавиша пошла вниз, keyup — вернулась. Между ними браузер делает своё дело: вставляет символ в поле, прокручивает страницу пробелом, отправляет форму по Enter. Отсюда правило: отменять поведение по умолчанию можно только в keydown — в keyup отменять уже нечего, всё случилось.
Оба события всплывают, поэтому глобальные хоткеи вешают один раз на document, а не на каждое поле. В target лежит элемент, который был в фокусе; если фокуса нет ни на чём, там окажется body.
В обработчике keydown поле хранит ещё старый текст: символ попадёт в input.value только после того, как браузер доведёт событие до конца. Нужен новый текст — слушайте input.
key — это что получилось: буква с учётом регистра и раскладки либо имя служебной клавиши ("Enter", "Escape", "ArrowLeft", а у пробела — просто " "). code — что нажали: неизменная метка позиции клавиши на клавиатуре ("KeyS", "Digit1", "Space", "ShiftLeft"). Одна клавиша, две разные истории.
| Клавиша | key | key с Shift | key на кириллице | code |
|---|---|---|---|---|
| Буква S | s | S | ы | KeyS |
| Цифра 1 | 1 | ! | 1 | Digit1 |
| Пробел | " " | " " | " " | Space |
| Стрелка влево | ArrowLeft | ArrowLeft | ArrowLeft | ArrowLeft |
| Левый Shift | Shift | Shift | Shift | ShiftLeft |
Правило простое: «что человек ввёл» — key; «на какую клавишу нажал» — code. Управление в игре на WASD только через code, иначе на кириллице герой встанет. А реакция на Escape — через key: это имя одинаково в любой раскладке.
code описывает позицию клавиши на условной US-раскладке железа. На французской AZERTY клавиша в позиции "KeyQ" подписана буквой A, так что «Ctrl+Z» по коду окажется совсем не там, где ждёт француз. Честный ответ — дать пользователю переназначить сочетания; в Chromium есть ещё navigator.keyboard.getLayoutMap(), которая возвращает реальные подписи клавиш.
Четыре булевых свойства события — ctrlKey, altKey, shiftKey, metaKey (Cmd на маке, Win на Windows) — говорят, что было зажато в момент нажатия. Сами модификаторы приходят и отдельными событиями: у Shift key равен "Shift", а code — "ShiftLeft" или "ShiftRight". Состояние залипающих клавиш спрашивают отдельно: e.getModifierState("CapsLock").
Один и тот же жест «сохранить» — это Ctrl+S на Windows и Linux и Cmd+S на маке, поэтому в проверке почти всегда стоит e.ctrlKey || e.metaKey.
Если клавишу держать, операционная система включает автоповтор: keydown прилетает снова и снова, а keyup — ровно один раз, когда палец отпустил. У повторных событий свойство repeat равно true, у самого первого — false.
Для стрелок в списке повтор — благо: человек держит клавишу и едет вниз. Для действий вроде «отправить» или «открыть окно» — беда, поэтому такие обработчики начинают с if (e.repeat) return;.
Классическая ошибка — проверка e.key === "b" && e.ctrlKey. На русской раскладке та же клавиша даёт "и", и хоткей перестаёт работать ровно тогда, когда человек пишет по-русски, то есть в самый нужный момент.
Сочетание удобно собирать в строку-ключ и искать её в таблице: весь набор хоткеев виден целиком и легко расширяется. И не забывайте preventDefault() — у браузера свои планы на Ctrl+S, Ctrl+P и Ctrl+F.
Событие keypress объявлено устаревшим: оно не приходит на служебные клавиши и по-разному ведёт себя в браузерах. Свойства keyCode, charCode и which устарели следом: их значение зависит и от браузера, и от события — в keydown это код клавиши, в keypress код символа, а по стрелке keypress не приходит вовсе. Сегодня клавиши ловят через keydown и keyup, а вводимый текст — через beforeinput и input, которые честно работают и со вставкой мышью, и с экранной клавиатурой телефона.
- keydown приходит до того, как браузер отработал нажатие, keyup — после; отменять поведение по умолчанию можно только в keydown.
- key — символ или имя служебной клавиши, зависит от раскладки и Shift; code — метка физической клавиши, не зависит ни от того, ни от другого.
- Модификаторы читаются флагами ctrlKey, altKey, shiftKey, metaKey; кроссплатформенная проверка — ctrlKey || metaKey.
- Зажатая клавиша сыплет keydown с repeat: true, а keyup приходит один раз; одноразовые действия начинают с if (e.repeat) return.
- Хоткеи с латинскими буквами вешают на code, реакцию на Enter и Escape — на key, а введённый текст ловят событием input.
1/3Пользователь переключился на русскую раскладку и нажал клавишу, подписанную латинской S. Что окажется в e.code?
2/3Стрелку вниз зажали и держат секунду. Что увидит обработчик?
3/3Какая проверка для «жирного шрифта» по Ctrl+B переживёт переключение раскладки?
Escape очищает, Enter отправляет
Повесьте на поле обработчик keydown: по Enter текущее значение попадает в массив sent, по Escape поле очищается. Остальные клавиши обработчик игнорирует.
Здесь работает именно key: имена "Enter" и "Escape" одинаковы в любой раскладке, а вот code у Enter на цифровом блоке был бы "NumpadEnter" — и клавиша перестала бы работать.
Enter приходит первым, поэтому в sent попадает ещё не очищенное значение, а после Escape поле остаётся пустым.
Хоткей Ctrl/Cmd+K без ложных срабатываний
Сделайте на document обработчик палитры команд: Ctrl+K или Cmd+K увеличивают opened.count. Автоповтор считать не нужно, одиночная K без модификатора — тоже, и русская раскладка не должна ломать сочетание.
Три отсечки подряд, каждая отвечает за свою беду: автоповтор, чужая клавиша, отсутствие модификатора.
Проверка по code делает хоткей независимым от раскладки: key в примере равен "л", но клавиша всё та же.