Указывая путь вперед

Sérgio Gomes

Раньше указывать на объекты в Интернете было просто. У вас была мышь, вы перемещали ее, иногда нажимали кнопки, и все. Все, что не было мышью, эмулировалось как мышь, и разработчики точно знали, на что рассчитывать.

Однако простота не обязательно означает хорошо. Со временем становилось все более важным, чтобы не все было (или притворялось) мышью: у вас могли быть чувствительные к давлению и наклону ручки, обеспечивающие потрясающую творческую свободу; вы могли пользоваться пальцами, поэтому все, что вам нужно, — это устройство и ваша рука; и эй, почему бы не использовать более одного пальца, пока ты это делаешь?

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

Модель одного события

События указателя унифицируют модель ввода указателя для браузера, объединяя сенсорный ввод, перья и мышь в единый набор событий. Например:

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

Вот список всех доступных событий, который должен показаться вам довольно знакомым, если вы знакомы с событиями мыши:

pointerover Указатель вошел в ограничивающую рамку элемента. Это происходит немедленно для устройств, поддерживающих наведение, или перед событием pointerdown для устройств, которые его не поддерживают.
pointerenter Похож на pointerover , но не всплывает и обрабатывает потомков по-другому. Подробности по спец .
pointerdown Указатель перешел в состояние активной кнопки: либо кнопка нажата, либо установлен контакт, в зависимости от семантики устройства ввода.
pointermove Указатель изменил положение.
pointerup Указатель вышел из активного состояния кнопки.
pointercancel Что-то произошло, что означает, что указатель вряд ли будет генерировать еще какие-либо события. Это означает, что вам следует отменить все выполняемые действия и вернуться в нейтральное состояние ввода.
pointerout Указатель вышел за пределы ограничивающей рамки элемента или экрана. Также после pointerup , если устройство не поддерживает наведение.
pointerleave Похож на pointerout , но не всплывает и обрабатывает потомков по-другому. Подробности по спец .
gotpointercapture Элемент получил захват указателя .
lostpointercapture Указатель, который был захвачен, освобожден.

Различные типы ввода

Как правило, события указателя позволяют писать код, не зависящий от ввода, без необходимости регистрировать отдельные обработчики событий для разных устройств ввода. Конечно, вам все равно нужно помнить о различиях между типами ввода, например, применима ли концепция наведения. Если вы хотите отличить разные типы устройств ввода друг от друга — возможно, чтобы предоставить отдельный код/функциональность для разных входов — вы можете сделать это из одних и тех же обработчиков событий, используя свойство pointerType интерфейса PointerEvent . Например, если вы кодировали боковой навигационный ящик, вы могли бы использовать следующую логику в событии pointermove :

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

Действия по умолчанию

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

При использовании событий указателя всякий раз, когда запускается действие по умолчанию, такое как прокрутка или масштабирование, вы получаете событие pointercancel , чтобы сообщить, что браузер взял на себя управление указателем. Например:

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

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

Вы можете запретить браузеру получать управление с помощью свойства CSS touch-action . Установка значения none для элемента отключит все определенные браузером действия, выполняемые над этим элементом. Но существует ряд других значений для более детального управления, например pan-x , позволяющий браузеру реагировать на движение по оси x, но не по оси y. Chrome 55 поддерживает следующие значения:

auto По умолчанию; браузер может выполнять любое действие по умолчанию.
none Браузеру не разрешено выполнять какие-либо действия по умолчанию.
pan-x Браузеру разрешено выполнять только действие по умолчанию с горизонтальной прокруткой.
pan-y Браузеру разрешено выполнять только действие по умолчанию с вертикальной прокруткой.
pan-left Браузеру разрешено выполнять только действие по горизонтальной прокрутке по умолчанию и только панорамировать страницу влево.
pan-right Браузеру разрешено выполнять только действие по горизонтальной прокрутке по умолчанию и только панорамировать страницу вправо.
pan-up Браузеру разрешено выполнять только действие по умолчанию с вертикальной прокруткой и только панорамировать страницу вверх.
pan-down Браузеру разрешено выполнять только действие по умолчанию с вертикальной прокруткой и только панорамировать страницу вниз.
manipulation Браузеру разрешено выполнять только действия прокрутки и масштабирования.

Захват указателя

Вы когда-нибудь тратили утомительный час на отладку неработающего события mouseup , пока не поняли, что это происходит потому, что пользователь отпускает кнопку за пределами цели клика? Нет? Ладно, тогда, возможно, это только я.

Тем не менее, до сих пор не было действительно хорошего способа решения этой проблемы. Конечно, вы можете настроить обработчик mouseup в документе и сохранить некоторое состояние в своем приложении, чтобы отслеживать происходящее. Однако это не самое чистое решение, особенно если вы создаете веб-компонент и пытаетесь сохранить все красиво и изолированно.

С событиями указателя есть гораздо лучшее решение: вы можете захватить указатель, чтобы быть уверенным, что получите это событие pointerup (или любое другое из его неуловимых друзей).

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

Поддержка браузера

На момент написания события Pointer Events поддерживаются в Internet Explorer 11, Microsoft Edge, Chrome и Opera и частично поддерживаются в Firefox. Актуальный список можно найти на сайте caniuse.com .

Вы можете использовать полифил Pointer Events для заполнения пробелов. Альтернативно, проверить поддержку браузера во время выполнения очень просто:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

События указателя — отличный кандидат для постепенного улучшения: просто измените методы инициализации, чтобы выполнить описанную выше проверку, добавьте обработчики событий указателя в блок if и переместите обработчики событий мыши/касания в блок else .

Так что давай, дайте им покрутиться и дайте нам знать, что вы думаете!