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

в 9:44, , рубрики: html, input mask, inputmask, javascript, masking, maskito, open source, TypeScript, web, Блог компании Tinkoff, Разработка веб-сайтов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

У 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. Это коллекция библиотек, написанных на Typescript. Главная библиотека @maskito/core создана без использования внешних зависимостей, что позволяет применять ее в любом vanilla JavaScript проекте. 

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

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

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js