- PVSM.RU - https://www.pvsm.ru -

Трудности маскирования текстового поля

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

Хороший UI/UX помогает пользователю избежать большинства таких проблем. Инструментов контроля огромное количество, сегодня расскажу про один их них — создание маски для поля ввода силами Javascript.

Трудности маскирования текстового поля - 1

Да что такое, эта ваша маска

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

Читатель может вспомнить, что в HTML уже есть <input type="number" />. Открываем наш любимый Chrome, страницу с документацией элемента [1], пробуем ввести что-то кроме цифр и точки или запятой… ура! Браузер запрещает это сделать, и при попытке ввода невалидного символа значение инпута не изменяется. Кажется, что проблема решилась быстро, пора и статью завершать на этой удачной находке! 

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

Трудности маскирования текстового поля - 2

То, что сделал Firefox в нашем примере, — это издевательство один из способов предупредить пользователя о невалидном значении. Но, кажется, не очень своевременный. А вот Chrome показал один из примеров маскирования инпута.

Трудности маскирования текстового поля - 3

Маска — это контролирование вводимых пользователем символов, чтобы значение текстового поля соответствовало определенному правилу или паттерну.

Пример с вводом чисел — один вид маски из множества возможных. Существуют более сложные примеры: ввод времени [2], даты [3] или телефонного номера. [4]

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

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

Маскирование инпута — это скорее про повышение UX, чем про валидацию данных. Опытный злоумышленник способен обмануть любое фронтовое веб-приложение. Поэтому дополнительная финальная валидация данных на беке всегда нужна!

Ингредиенты маски

Нужно разобраться в большом списке событий, которые возникают у элементов <input /> и <textarea />, чтобы контролировать вводимые значения в текстовом поле. Расскажу об основных.

Keydown [5] — событие, которое возникает каждый раз при нажатии любой клавиши с клавиатуры. Оно содержит полезное свойство key, в котором и хранится информация о введенном значении. И самое главное — событие можно отменить через event.preventDefault!

Кажется, что такое событие идеально подходит для маскирования инпутов и полностью закрывает все необходимые задачи. Но есть два недостатка:

  • Существование системных клавиш создает ряд проблем в использовании события для нашей задачи. Например, пользователь будет копировать значение инпута через Ctrl + C и получится «ложноположительное» для нашей задачи срабатывание keydown. Требуется много усилий, чтобы отфильтровать нужные события для маски. 

  • Событие не может контролировать ввод значений через браузерный автофил и выбор предлагаемого значения с нативной клавиатуры мобильного устройства.

А еще для нашей задачи при использовании keydown придется звать на помощь paste и drop события, которые обсудим чуть позже.

Keypress [6] — событие, идентичное keydown-событию, но с одним приятным исключением: оно срабатывает только при нажатии клавиш, порождающих новое значение в поле ввода. Keypress не стреляет ненужным нам нажатием системных клавиш и полностью решает первый недостаток keydown-события. 

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

Paste [7] и Drop [8] — события, про которые часто забывают. Пользователь может изменить значение текстового поля не только нажатием клавиш с клавиатуры, но и это может происходит не только через знаменитую комбинацию <code>Ctrl</code> + <code>V</code>, но и через контекстное меню и прочие способы</p>" data-abbr="через вставку из буфера обмена">через вставку из буфера обмена и сбросив текст в инпут. Поэтому paste и drop нужно использовать, когда маскирование инпута происходит с обработкой keydown.

Change [9] — сообщает об изменении значения инпута. Но момент срабатывания события для текстовых инпутов происходят только после потери фокуса. То есть, если пользователь сфокусируется на инпуте и попытается напечатать слово «привет», у инпута сработает шесть событий keydown и только одно change — при условии, что пользователь все же уберет фокус с поля. Несмотря на многообещающее название и хорошую поддержку браузерами, это событие нам не подходит.

Input [10] — полезное для нас событие, которое решает многие проблемы упомянутых ранее «коллег». Плюсы события:

  • Input срабатывает после каждого изменения значения текстового инпута, не дожидается потери фокуса, как Change, и не требует прочих условий. 

  • Нажатие системных клавиш не триггерит событие, если значение не меняется. 

  • Контролирует все манипуляции с текстовым полем: событие сработает и при вставке значения из буфера обмена, и при браузерном автозаполнении, и при сбрасывании текста в инпут мышкой, и при выборе подсказок с нативной мобильной клавиатуры. 

  • Хорошая поддержка всеми современными браузерами. 

Ложка дегтя с Input в том, что событие нельзя отменить свойством preventDefault, потому что событие сообщает об уже случившимся факте. А прошлое изменить нельзя! 

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

В интернете много библиотек, упрощающих маскирование текстовых полей. Большинство популярных «взрослых» решений используют комбинации описанных нативных событий со всеми преимуществами и недостатками. Но что, если бы появилась необходимость создать новую библиотеку в 2023 году? Повторила бы она опыт своих предшественников? Есть припрятанный туз в рукаве, который мы еще не успели обсуди, — beforeinput-событие.

Рецепт современной маски

Beforeinput [11] — молодое событие, которое идеально подходит для маскирования инпутов. В марте 2017 года его подарил нам… Safari. Да, этот браузер умеет не только вызывать слезы фронтенд-разработчиков, но иногда и первым радовать их новыми фичами. 

Следующим это свойство реализовал Chrome, а позже подхватили и другие браузеры. Отстающим крупным игроком стал Firefox, который обеспечил поддержку события лишь к 2021 году. На момент написания статьи beforeinput имеет хорошую поддержку современными браузерами и работает в 94,59% [12] от всех используемых версий браузеров.

У beforeinput масса достоинств для нашей задачи:

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

  • Поддерживает все прочие возможности изменить инпут помимо взаимодействия с клавиатуры — у события есть поле inputType, которое может принимать различные значения: insertText, insertFromDrop, insertFromPaste, deleteContentBackward, deleteContentForward и др. 

  • Событие можно отменить. 

Кажется, что взяли все самое лучше от прошлых событий и объединили все в новом!

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

element.addEventListener('beforeinput', event => {
   switch (event.inputType) {
       case 'deleteContentBackward':
       case 'deleteContentForward':
       case 'deleteByCut':
           return handleDelete(event);
       case 'insertLineBreak':
           return handleEnter(event);
       case 'insertFromPaste':
       case 'insertText':
           return handleInsert(event);
       // ...
       // Many other cases
       // ...
   }
});

Подчеркну важную особенность. Если нужно отменить beforeinput-событие — например, чтобы самостоятельно программно обновить инпут нужным значением, то отменится после него и последующее input-событие. Такое поведение ожидаемо с точки зрения логики. 

Иногда мы хотим сообщить о случившемся внешним наблюдателям. Хорошим примером может стать фреймворк Angular: у него есть свои инструменты для работы с формами, которые полагаются на факт, что событие input произойдет на каждое изменение значения инпута. Проблема имеет множество решений, одно из них — при отмене beforeinput-события с последующим программным обновлением текстового поля можно самостоятельно сделать element.dispatchEvent(new InputEvent(...)).

Трудности маскирования текстового поля - 4

Коллекция библиотек Maskito

Выявленную формулу для создания масок мы применили в новой разработке Maskito. [13] Это коллекция библиотек, написанных на Typescript. Главная библиотека @maskito/core создана без использования внешних зависимостей, что позволяет применять ее в любом vanilla JavaScript проекте. 

В Maskito есть библиотека @maskito/kit — набор уже готовых конфигурируемых масок. А еще мы создали отдельный опциональный пакет @maskito/angular на случай, если вы захотите использовать разработку в своем Angular-проекте. Подробнее обо всех возможностях Maskito читайте в документации. [14] А в следующей статье расскажу подробнее, что у нас из этого получилось, и покажу немного реального кода.

Maskito уже публикуется в npm под нулевой мажорной версией и готово к использованию. Мы проводим финальные тесты, ищем и исправляем баги, чтобы совсем скоро опубликовать первую мажорную версию. Сохраняйте библиотеку в заметки — надеемся, что она пригодится вам в следующем проекте!

Автор: Барсуков Никита

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/383918

Ссылки в тексте:

[1] страницу с документацией элемента: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number

[2] времени: https://tinkoff.github.io/maskito/kit/time

[3] даты: https://tinkoff.github.io/maskito/kit/date

[4] телефонного номера.: https://tinkoff.github.io/maskito/recipes/phone

[5] Keydown: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event

[6] Keypress: https://developer.mozilla.org/en-US/docs/Web/API/Element/keypress_event

[7] Paste: https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event

[8] Drop: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event

[9] Change: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event

[10] Input: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event

[11] Beforeinput: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event

[12] работает в 94,59%: https://caniuse.com/?search=beforeinput

[13] в новой разработке Maskito.: https://github.com/Tinkoff/maskito

[14] в документации.: https://tinkoff.github.io/maskito

[15] Источник: https://habr.com/ru/companies/tinkoff/articles/727368/?utm_source=habrahabr&utm_medium=rss&utm_campaign=727368