[Из песочницы] Антистресс-маска

image

Речь пойдет об очередном решении многолетней проблемы в браузере — ограничение пользовательского ввода, или просто — маска, которая используется повсеместно: номера телефонов, кредитных карт, паспорта и т.д.

На данный момент было найдено два популярных решения:

  1. jQuery.Inputmask
  2. jQuery-Mask-Plugin

Те, кто пытался использовать маски в своих и без того непростых проектах, скорее всего были бы рады выбросить все это дело и использовать просто валидацию. Особенно если маска должна быть динамической, зависеть от уже введенных символов, нужна возможность получать размаскированное значение даже если пользователь ввел его не целиком, или нужно полностью скрыть placeholder… Что работало в одной библиотеке — не работало в другой, как только извращаться не приходилось. Уж проще самому написать, в конце то концов, программисты мы или кто?! Да и коллеги тоже не потерялись, написали под Android же.

Кому не терпится, вот оно: imaskjs.
Поломать демку можно здесь.

А я продолжу по порядку. Что получилось:

  • нет внешних зависимостей
  • в любой момент можно получить или установить сырое или размаскированное значение
  • опциональные символы (жадный алгоритм)
  • размаскированные значения могут содержать фиксированные части маски
  • 2 режима отображения placeholder: постоянный и ленивый (отсутствует, но появляется только тогда, когда обязательные символы ввода находятся в середине текста и сместить символы справа на их место нельзя)
  • IE11+ из коробки или см. ниже

Кратко как использовать:
var element = document.getElementById('selector');
var mask = new IMask(element, {
  mask: '+{7}(000)000-00-00'
});
mask.rawValue = '999-12-12-123';
console.log(mask.unmaskedValue);  // 79991212123

Где, mask может быть регуляркой (нет, я не сделал свой движок), шаблоном, функцией, либо наследником определенного класса.

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

Итак, немногочисленные правила шаблона:

0 — вводимая цифра
a — вводимая буква
* — вводимый любой символ
[] — делает ввод опциональным
{} — делает фиксированные символы частью размаскированного значения

А еще можно подписываться на события, использовать собственные функции для проверки и др. Подробнее в документации.

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

Особенности работы


Основной задачей было создать минимальный работающий инструмент.

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

При конфликте вставки пропускаются символы вставляемого значения. Если символ совпадает с фиксированным — он попадает на его место при вставке сырого значения, либо является частью ввода. Самый очевидный и простой вариант. Возможны более сложные не жадные алгоритмы, но оно не стоит того.

Указание опциональности относится только к вводимым символам, фиксированные символы внутри [] по прежнему остаются частью placeholder’а, если он должен отображаться. Иначе не всегда можно определить когда показывать фиксированные символы (например:»0[00–0]0» — не очевидно в каких случаях отображать »-»).

Также существует проблема в случае, когда обязательные или фиксированные символы идут после опциональных, при этом их определения совпадают, т.е. символ может находится в любой позиции. Если попытаться наложить такую маску на значение возникает множество возможных очевидных и не очень случаев. Например если в начало поля ввода с маской »[000]01110222», которая отображается в виде »_111_222» вставить значение »21113222», то логично было бы увидеть его же, но поскольку вначале идут не опциональные символы, то значение окажется »21111113222». Решение в общем виде нетривиально. Судя по всему, для расстановки требуется дополнительная информация о «дырках». Но я вижу проблему больше в самой постановке задачи — для этого маски не предназначены, поэтому сделано проще — при вставке значения все символы обладают одинаковым приоритетом.

Выключен on drop. Решение есть, но выбивается из общего подхода (см. ниже).

Одной из опций в библиотеках-аналогах — это возможность указывать количество повторяемых символов. Например jquery.inputmask позволяет указывать количество символов:

Inputmask("9-a{1,3}9{1,3}").mask(selector); // от 1 до 3 символов
Inputmask("9", { repeat: 10 }).mask(selector);  // от 0 до 10
Inputmask("9{*}").mask(selector); // и даже сколько угодно!

Вряд ли повтор символов является функционалом маски, да и ES6 давно на дворе. А насчет бесконечных масок — отдельный разговор.

Существует великий соблазн сделать из маски навороченный комбайн для всех возможных вариантов: «Ведь пользователь может и с »+7» начать и »8» захотеть ввести…» Поубивал бы!
В моем случае возможность альтернативных частей маски не реализована, но существующие решения явно не устраивают.

Например, в jquery.inputmask можно сделать так:

Inputmask("(aaa)|(999)").mask(selector);  // либо 3 буквы, либо 3 цифры

Только для случаев посложнее приходится сочинять свои группы символов (например для дат), и обозначенное автором преимущество алиасов в «сокрытии сложности» только ее добавляет. Да и сам формат становится неудобным.

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

Разбор внутренностей


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

Итак, в отличие от коллег, я не стал создавать сложный объект для каждого символа маски (Slot в аналоге), а пожертвовал этим уровнем абстракции и сделал вот как:

  • Строковое описание маски преобразуется в неизменяемый список объектов с полями:
    1. символ (используется для фиксированных символов)
    2. тип (фиксированный, либо ввод)
    3. опциональный или нет
    4. должен ли включаться в размаскированное значение
    Один объект на каждый символ маски.
  • Описания символов (definitions) преобразуются в неизменяемый объект вида {<definition><BaseMask object>}, где BaseMask, как вы уже наверно догадались, рекурсивно определенная маска или проще — валидатор для конкретного типа символа.
  • Хранится изменяемый список «дырок» — незаполненных символов ввода.
  • Хранится последнее корректное состояние — значение поля и положение курсора.

Теперь о динамике


Перед любым изменением нужно сохранить состояние. По умолчанию — на событие keydown.

Далее даем пользователю ввести все, что он хочет, а после — корректируем. По умолчанию — на событие input.

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

Кратко про самую интересную часть алгоритма


При применении маски в первую очередь определяется интервал изменений (изменения — это вставка, удаление или замена). Символы, стоящие до интервала изменений, считаются корректными, и подставляются как есть. Из подстроки после интервала изменений, извлекаются все символы ввода.

Затем, если была вставка, то подставляется максимально возможное количество вставленных символов с учетом извлеченных символов ввода после интервала. Если было удаление — символы удаляются и подставляется минимально необходимый placeholder.

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

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

Как одно из особенностей получившегося решения (хотя скорее всего это особенность всех решений с масками) — необходимость уведомлять маску, если значение просочилось мимо библиотеки. Например, другой плагин подсовывает значение непосредственно в свойство value элемента. В таких случаях необходимо вручную вызывать метод refresh для наложения маски.

Вместо заключения


Выключил drop, взял keydown и input, и никаких хитрых хаков с кодами клавиш и прочей нечистью. Даже на paste не завязывался.

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

Библиотека еще довольна сырая, но надеюсь начнет скоро экономить нервы.
Буду рад замечаниям и предложениям.

Комментарии (0)

© Habrahabr.ru