16 липня 2023 р.

Клавіатура: keydown та keyup

Перш ніж ми перейдемо до клавіатури, зверніть увагу, що на сучасних пристроях є й інші способи “щось ввести”. Наприклад, люди використовують розпізнавання мовлення (особливо на мобільних пристроях) або копіюють/вставляють за допомогою миші.

Тож якщо ми хочемо відстежувати будь-яке введення даних у поле <input>, то подій клавіатури недостатньо. Існує ще одна подія під назвою input для відстеження змін у полі <input> будь-яким способом. І це може бути кращим рішенням такого завдання. Ми розглянемо цю подію пізніше в розділі Події: change, input, cut, copy, paste.

Події клавіатури слід використовувати, коли ми хочемо обробляти дії клавіатури (віртуальна клавіатура також враховується). Наприклад, щоб реагувати на клавіші зі стрілками Up та Down або гарячі клавіші (включаючи комбінації клавіш).

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

Щоб краще зрозуміти події на клавіатурі, ви можете використовувати тестовий стенд, наведений нижче.

Спробуйте різні комбінації клавіш у текстовому полі.

Результат
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">

    Запобігти типовій дії для:
    <label>
      <input type="checkbox" name="keydownStop" value="1"> keydown</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="keyupIgnore" value="1"> keyup</label>
    </p>

    <p>Поставте фокус на полі введення та натисніть клавішу.</p>

    <input type="text" placeholder="Натисніть клавіші тут" id="kinput">

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


</body>
</html>

Keydown та keyup

Події keydown відбуваються, коли клавішу натискають, а потім keyup, коли її відпускають.

event.code та event.key

Властивість key об’єкта події дозволяє отримати символ, а властивість code об’єкта події дозволяє отримати “фізичний код клавіші”.

Наприклад, ту саму клавішу Z можна натискати з або без клавіші Shift. Це дає нам два різних символи: малий z і великий Z.

event.key – це саме символ, і він може бути різним. Але event.code однаковий:

Клавіша event.key event.code
Z z (нижній регістр) KeyZ
Shift+Z Z (верхній регістр) KeyZ

Якщо користувач працює з різними мовами, то при перемиканні на іншу мову замість "Z" буде зовсім інший символ. Він стане значенням event.key, тоді як event.code буде завжди однаковим: "KeyZ".

“KeyZ” та інші коди клавіш

Кожна клавіша має код, який залежить від її розташування на клавіатурі. Ключові коди, описані в Специфікації коду UI подій.

Наприклад:

  • Клавіші літер мають коди "Key<літера>": "KeyA", "KeyB" і т.д.
  • Цифрові клавіші мають коди: "Digit<цифра>": "Digit0", "Digit1" і т.д.
  • Спеціальні клавіші кодуються своїми назвами: "Enter", "Backspace", "Tab" тощо.

Існує кілька поширених розкладок клавіатури, і специфікація містить коди клавіш для кожної з них.

Прочитайте алфавітно-цифровий розділ специфікації, щоб отримати додаткові коди, або просто натисніть клавішу на тестовому стенді вище.

Регістр має значення: "KeyZ", а не "keyZ"

Здається очевидним, але більшість все одно припускається помилок.

Будь ласка, уникайте помилок: це KeyZ, а не keyZ. Перевірка на кшталт event.code=="keyZ" не працюватиме: перша літера "Key" має бути у верхньому регістрі.

Що робити, якщо клавіша не дає жодного символу? Наприклад, Shift або F1 або інші. Для цих клавіш event.key приблизно такий самий, як event.code:

Клавіша event.key event.code
F1 F1 F1
Backspace Backspace Backspace
Shift Shift ShiftRight або ShiftLeft

Зверніть увагу, що event.code точно визначає, яка клавіша натиснута. Наприклад, більшість клавіатур мають дві клавіші Shift: ліворуч і праворуч. event.code говорить нам, яка саме була натиснута, а event.key відповідає за “значення” клавіші: яка це саме клавіша (це “Shift”).

Скажімо, ми хочемо обробити гарячу клавішу: Ctrl+Z (або Cmd+Z для Mac). Більшість текстових редакторів підключають до неї дію “Скасувати”. Ми можемо встановити прослуховувач на keydown і перевірити, яка клавіша натиснута.

Тут виникає дилема: у такому прослуховуванні ми перевіряємо значення event.key або event.code?

З одного боку, значення event.key є символом, воно змінюється залежно від мови. Якщо відвідувач має кілька мов в ОС і перемикається між ними, одна і та ж клавіша дає різні символи. Тому має сенс перевірити event.code, він завжди однаковий.

Ось таким чином:

document.addEventListener('keydown', function(event) {
  if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
    alert('Скасувати!')
  }
});

З іншого боку, є проблема з event.code. Для різних розкладок клавіатури одна і та ж клавіша може мати різні символи.

Наприклад, ось розкладка США (“QWERTY”) і німецька (“QWERTZ”) під нею (з Вікіпедії):

Для тієї ж клавіші американська розкладка має “Z”, а німецька — “Y” (букви змінюються місцями).

Без перебільшень, event.code буде дорівнювати KeyZ для людей з німецькою розкладкою, коли вони натиснуть Y.

Якщо ми перевіримо event.code == 'KeyZ' у нашому коді, то для людей з німецькою розкладкою такий тест пройде, коли вони натиснуть Y.

Звучить дуже дивно, але так і є. У специфікації чітко згадується така поведінка.

Отже, event.code може збігатися з неправильним символом для неочікуваної розкладки. Одні й ті самі букви в різних розкладках можуть зіставлятися з різними фізичними клавішами, що призводить до різних кодів. На щастя, це відбувається лише з кількома кодами, напр. keyA, keyQ, keyZ (як ми бачили), і не відбувається зі спеціальними клавішами, такими як Shift. Список можна знайти в специфікації.

Для надійного відстеження символів, що залежать від розкладки, event.key може бути кращим рішенням.

З іншого боку, event.code має перевагу, бо залишається завжди однаковим, прив’язаним до місця розташування фізичної клавіші, навіть якщо відвідувач змінює мову. Тому гарячі клавіші, які покладаються на нього, працюють добре навіть у разі перемикання мови.

Чи хочемо ми обробляти клавіші, що залежать від розкладки? Тоді event.key – це вихід.

Або ми хочемо, щоб гаряча клавіша працювала навіть після зміни мови? Тоді event.code може бути кращим рішенням.

Автоповтор

Якщо клавіша натискається досить довго, вона починає автоматично повторюватися: keydown запускається знову і знову, а потім, коли вона відпускається, ми нарешті отримуємо keyup. Таким чином, це нормально мати багато keydown і один keyup.

Для подій, ініційованих автоповтором, об’єкт події має властивість event.repeat, встановлену на true.

Типові дії

Типові дії можуть бути різними, оскільки існує багато можливих речей, які можуть бути ініційовані клавіатурою.

Наприклад:

  • На екрані з’являється символ (найбільш очевидний результат).
  • Символ видалено (Delete клавіша).
  • Сторінка прокручується (PageDown клавіша).
  • Браузер відкриває діалогове вікно “Зберегти сторінку”. (Ctrl+S)
  • …і так далі.

Запобігання типовій дії на keydown може скасувати більшість з них, за винятком спеціальних клавіш на основі ОС. Наприклад, у Windows Alt+F4 закриває поточне вікно браузера. І немає способу зупинити це, запобігаючи типовій дії у JavaScript.

Наприклад, <input> нижче очікує номер телефону, тому він не приймає клавіші, крім цифр, +, () або -:

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') || ['+','(',')','-'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Номер телефону, будь ласка" type="tel">

Обробник onkeydown використовує тут checkPhoneKey для перевірки натиснення клавіші. Якщо вона валідна (від 0..9 або з +-()), обробник повертає true, інакше false.

Як ми знаємо, значення false, що повертається з обробника подій та присвоєне за допомогою властивості DOM або атрибута, як-от вище, запобігає типовій дії. Саме тому для клавіш, які не проходять тест, у <input> нічого не з’являється. (Повернуте значення true ні на що не впливає, має значення лише повернення false)

Зверніть увагу, що спеціальні клавіші, такі як Backspace, Left, Right, не працюють у полі для введення. Це побічний ефект суворого фільтра checkPhoneKey. Ці клавіші змушують її повертати false.

Давайте трохи послабимо фільтр, дозволивши клавіші зі стрілками Left, Right, Backspace та Delete:

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') ||
    ['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Номер телефону, будь ласка" type="tel">

Тепер стрілки та видалення працюють добре.

Незважаючи на те, що у нас є фільтр клавіш, все одно можна ввести будь-що за допомогою миші та клацнути правою кнопкою миші + “Вставити”. Мобільні пристрої надають інші засоби для введення значень. Тому фільтр не є надійним на 100%.

Альтернативним підходом було б відстежувати подію oninput – вона запускається після будь-якої модифікації. Там ми можемо перевірити новий input.value та змінити його/виділити <input>, якщо він недійсний. Або ми можемо використовувати обидва обробники подій разом.

Застарілий код

У минулому була подія keypress, а також keyCode, charCode, which властивості об’єкта події.

Під час роботи з ними було так багато несумісностей у браузерах, що у розробників специфікації не було іншого виходу, окрім як відмовитися від усіх них і створити нові, сучасні події (описані вище в цьому розділі). Старий код все ще працює, оскільки браузери продовжують підтримувати його, але більше немає потреби використовувати його.

Мобільні клавіатури

Під час використання віртуальних/мобільних клавіатур, офіційно відомих як IME (Input-Method Editor), стандарт W3C стверджує, що у події клавіатури e.keyCode має бути 229, а e.key має бути "Unidentified".

Хоча деякі з цих клавіатур все ще можуть використовувати правильні значення для e.key, e.code, e.keyCode… під час натискання певних клавіш, таких як стрілки або видалення, немає гарантії, тому ваша логіка клавіатури може не завжди працювати на мобільних пристроях.

Підсумки

Натискання клавіші завжди генерує подію на клавіатурі, будь то клавіші-символи або спеціальні клавіші, як-от Shift або Ctrl тощо. Єдиним винятком є клавіша Fn, яка іноді зустрічається на клавіатурі ноутбука. Для неї немає жодних подій клавіатури, оскільки вони часто реалізуються на нижчому рівні, ніж ОС.

Події клавіатури:

  • keydown – при натисканні клавіші (автоматично повторюється, якщо клавіша довго утримується),
  • keyup – при відпусканні клавіші.

Основні властивості подій клавіатури:

  • code – “код клавіші” ("KeyA", "ArrowLeft" і так далі), специфічний для фізичного розташування клавіші на клавіатурі.
  • key – символ ("A", "a" і так далі), для несимвольних ключів, таких як Esc, зазвичай має те саме значення, що і code.

У минулому події клавіатури іноді використовувалися для відстеження даних, які користувач ввів у поля форми. Це ненадійно, оскільки вхідні дані можуть надходити з різних джерел. У нас є події input та change для обробки будь-якого введення (розглянуто далі в розділі Події: change, input, cut, copy, paste). Вони запускаються після будь-якого типу введення, включаючи копіювання або розпізнавання мовлення.

Ми повинні використовувати події клавіатури, коли нам дійсно потрібна клавіатура. Наприклад, реагувати на гарячі або спеціальні клавіші.

Завдання

важливість: 5

Створіть функцію runOnKeys(func, code1, code2, ... code_n), яка запускає func при одночасному натисканні клавіш із кодами code1, code2, …, code_n.

Наприклад, код нижче показує alert, коли "Q" та "W" натискаються разом (будь-якою мовою, з або без CapsLock)

runOnKeys(
  () => alert("Привіт!"),
  "KeyQ",
  "KeyW"
);

Демонстрація в новому вікні

Ми повинні використовувати два обробники: document.onkeydown і document.onkeyup.

Давайте створимо набір pressed = new Set(), щоб зберегти поточні натиснуті клавіші.

Перший обробник додає до нього, а другий видаляє. Кожного разу при keydown ми перевіряємо, чи достатньо натиснутих клавіш, і запускаємо функцію, якщо це так.

Відкрити рішення в пісочниці.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)