September 16th, 2021

JavaScript: двигаем элемент стрелками клавиатуры

Решил задачу «Мышь, управляемая клавиатурой» к подразделу 4.2 «Фокусировка: focus/blur» второй части учебника по JavaScript.

Название задачи сначала кажется странным (но вообще задача получилась веселая). Что значит «мышь, управляемая клавиатурой»? Тут путаница: речь идет не о компьютерной мыши (устройстве для взаимодействия с компьютером), а о мыши (мелком животном, грызуне), нарисованной на тестовой HTML-странице. Вот как эта HTML-страница выглядит в моём браузере (картинка):



Тонкая серая линия слева и сверху — это край окна браузера. Я оставил эту линию на рисунке для того, чтобы можно было понять, где относительно края области просмотра находится содержимое тестовой HTML-страницы.

«Мышь» на тестовой HTML-странице представлена HTML-элементом pre с идентификатором mouse, внутри которого средствами ASCII-графики (вики) нарисована мышь.

Задача состоит в том, чтобы написать скрипт на языке JavaScript, который позволит пользователю двигать HTML-элемент (в данном случае — HTML-элемент pre) по экрану компьютера с помощью клавиш со стрелками (вправо, влево, вверх и вниз) на клавиатуре.

На тестовой HTML-странице описаны два стиля на языке CSS для целевого HTML-элемента pre, представляющего мышь:
#mouse {
  display: inline-block;
  cursor: pointer;
  margin: 0;
}

#mouse:focus {
  outline: 1px dashed black;
}

При наведении указателя мыши на «мышь» на тестовой HTML-странице указатель мыши сменит свой вид со стрелки на руку с указующим перстом (указание cursor: pointer;). Это общепринятый способ показать пользователю, что в этом месте HTML-страницы он (пользователь) может воспользоваться дополнительным функционалом (в данном случае — двигать «мышь» клавишами-стрелками с клавиатуры).

«Мышь» на тестовой HTML-странице можно будет (по условиям задачи) двигать клавишами с клавиатуры только после установки фокуса на HTML-элемент pre, представляющий «мышь». При установке на него фокуса этот HTML-элемент будет обведен рамкой (черная прерывистая линия толщиной в 1 пиксель). Это описано с помощью указания outline: 1px dashed black; в соответствующем стиле, приведенном выше.

Код тестовой HTML-страницы можно посмотреть в песочнице. Также есть демонстрационный пример на отдельной странице.

* * *

Приступим к решению задачи. Я открыл тестовую HTML-страницу в своем браузере и попробовал установить фокус на HTML-элемент pre, представляющий мышь. Это у меня не получилось. По умолчанию пользователь не может установить фокус на данный HTML-элемент. Однако, эту возможность можно включить.

По условиям задачи запрещено вносить изменения в код тестовой HTML-страницы на языке HTML или с помощью стилей CSS. Сделаем это в скрипте на языке JavaScript с помощью свойства tabIndex:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

Теперь на тестовой HTML-странице я могу установить фокус на «мышь» либо кликнув по ней компьютерной мышью, либо с помощью клавиши Tab на клавиатуре. Установка фокуса отображается визуально появлением рамки из черной прерывистой линии толщиной в 1 пиксель, о которой я уже писал выше.

Отмечу два момента. Во-первых, нужно обратить внимание на заглавную букву I в названии свойства tabIndex. Именно так и следует писать в коде, иначе данная инструкция не сработает. А в коде HTML названия свойств можно писать по-разному, как захочется: хоть tabIndex, хоть tabindex и так далее. Названия свойств в HTML регистронезависимы.

Во-вторых, я создал переменную elem, чтобы сделать код более универсальным. Ведь вместо «мыши» нам может понадобиться передвинуть таким же образом любой HTML-элемент с любым рисунком в нем. То есть там может оказаться, скажем, не «мышь», а «кошка» или еще кто-то другой.

Дальше я стал думать, как отловить нажатия клавиш на целевом HTML-элементе. Сначала я подумал, что нужно как-то использовать событие focus или событие click, но потом понял, что можно просто использовать событие keydown на нужном HTML-элементе. Нажатия клавиш по умолчанию будут отлавливаться на HTML-элементе, только если на нем установлен фокус.

Дополним код:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

elem.addEventListener("keydown", function (event) {
    //...
});

В данном случае функция-обработчик события keydown отлавливает нажатие на любую клавишу клавиатуры. Нам же нужно отловить только нажатия на клавиши-стрелки. Исправим это, дополнив код:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

elem.addEventListener("keydown", function (event) {
    if (event.code != "ArrowRight" && event.code != "ArrowLeft" &&
        event.code != "ArrowUp" && event.code != "ArrowDown") return;

    //...
});

Такой фильтр пройдут только нажатия на четыре нужные клавиши-стрелки. Теперь осталось лишь реализовать движение целевого HTML-элемента, представляющего «мышь». Я решил включить для данного HTML-элемента абсолютное позиционирование. Движение HTML-элемента будет реализовано изменением координат этого HTML-элемента. HTML-элемент будет двигаться в указанную сторону не попиксельно (это слишком медленно), а сразу на свою ширину (влево или вправо) или на свою высоту (вверх или вниз). Дополним код:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

elem.addEventListener("keydown", function (event) {
    if (event.code != "ArrowRight" && event.code != "ArrowLeft" &&
        event.code != "ArrowUp" && event.code != "ArrowDown") return;

    let rectElem = elem.getBoundingClientRect();
    let x = rectElem.x + pageXOffset,
        y = rectElem.y + pageYOffset;

    if (event.code == "ArrowRight") x += elem.offsetWidth;
    if (event.code == "ArrowLeft")  x -= elem.offsetWidth;
    if (event.code == "ArrowUp")    y -= elem.offsetHeight;
    if (event.code == "ArrowDown")  y += elem.offsetHeight;
    
    elem.style.position = "absolute";
    elem.style.left = x + "px";
    elem.style.top = y + "px";
});

Этот код уже работает так, как требуется в задаче. Установив на «мышь» фокус, можно двигать ее клавишами-стрелками с клавиатуры. Если фокус с «мыши» убрать, ее нельзя будет двигать клавишами-стрелками с клавиатуры.

Однако, у этого кода есть один недостаток: когда «мышь» добегает до правой границы области просмотра браузера и заходит за нее, появляется горизонтальная полоса прокрутки. Если «мышь» добегает до нижней границы области просмотра браузера и заходит за нее, появляется вертикальная полоса прокрутки. На демонстрационной странице от авторов задачи такого не происходит.

Чтобы избавиться от этого эффекта, я решил запретить прокрутку на тестовой HTML-странице. Дополним код (я отметил изменение красным цветом):
let elem = document.getElementById("mouse");
elem.tabIndex = 0;
document.body.style.overflow = "hidden";

elem.addEventListener("keydown", function (event) {
    if (event.code != "ArrowRight" && event.code != "ArrowLeft" &&
        event.code != "ArrowUp" && event.code != "ArrowDown") return;

    let rectElem = elem.getBoundingClientRect();
    let x = rectElem.x + pageXOffset,
        y = rectElem.y + pageYOffset;

    if (event.code == "ArrowRight") x += elem.offsetWidth;
    if (event.code == "ArrowLeft")  x -= elem.offsetWidth;
    if (event.code == "ArrowUp")    y -= elem.offsetHeight;
    if (event.code == "ArrowDown")  y += elem.offsetHeight;
    
    elem.style.position = "absolute";
    elem.style.left = x + "px";
    elem.style.top = y + "px";
});

Это окончательный вариант скрипта. Теперь «мышь» просто забегает за границу области просмотра и исчезает из области видимости. Но ее можно вернуть обратно.

В прошлом веке, кстати, такими простыми методами и делали игры.

Мяу:
                   /)
          /\___/\ ((
          \`@_@'/  ))
          {_:Y:.}_//
----------{_}^-'{_}----------

Источник ASCII-картинки: https://www.asciiart.eu/animals/cats