JQuery плагин для валидации и форматирования денежных значений

Ильдар Сарибжанов | 23.01.2017

Задача

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

Простое решение

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

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

Свое решение

Я уже однажды сталкивался с проблемой валидации поля input. Если кратко — это не самая очевидно решаемая задача. В моем случае сам алгоритм решения задачи прост:

  1. Делим введенное значение на 2 части: слева от разделителя целая часть, справа — дробная
  2. У целой части рекурсивно или в цикле отрезаем по 3 символа справа и добавляем пробел
  3. У дробной части отсекаем по 3 символа слева и добавляем пробел

Собственно сама логика реализуется вот так:

function format( val ) {
	var has_separator = false,
	    res, int, fraction;

	val = val.split(',');

	// форматирование целой части
	while ( val[0].length > 3 ) {
		int    = val[0].substr(val[0].length - 3) + ' ' + int;
		val[0] = val[0].substr(0, val[0].length - 3);
	}
	int = val[0] + ' ' + int;

	// Последний пробел справа убираем
	res = int.replace(/\s$/g, '');
	res = res.replace(/^0/g, '');

	// форматирование дробной части, при наличии
	if( val.length > 1 ) {
		has_separator = true;
		while ( val[1].length > 3 ) {
			fraction += ' ' + val[1].substr(0, 3);
			val[1] = val[1].substr(3);
		}
		fraction = fraction + ' ' + val[1];

		// сначала удалим пробел
		fraction = fraction.replace(/^\s/g, '');
	}

	if( has_separator ) {
		res += ',';
	}

	if( fraction !== '' ) {
		res += fraction;
	}
	return res;
}

Вот как бы и чики-бамбони, что в переводе значит PROFIT. И тут вы спросите: «Эй, уважаемый, что за омлет, а где же яйца?!» — как этот код будет работать в рамках html+js. Вот мы и пришли к основной проблеме. Вопрос вот в чем: как использовать вышеописанную функцию? При вводе символа в input происходит ряд событий, которые можно перехватить, это keydown, keypress и keyup. Логичным предположением было, повесить обработчик на keypress и, когда это событие произойдет, передать значение input’а в функцию на обработку. Именно так я и сделал. Осознание ошибки пришло мгновенно: в момент вызова обработчика keypress, в поле ввода еще нет того символа, который мы только что ввели. Сюрприз-сюрприз! Конечно, если вы очень хорошо понимаете событийную модель, для вас это не новость, и для меня это была не новость, но когда идет поток сознания некоторые детали реализации могут ускользать. «И что делать?» — спросите вы, а я вам отвечу! (Обожаю задавать вопросы от вымышленного оппонента). «Терпение, спокойствие, сейчас они появятся». Забегая вперед, скажу, что гений меня в этот момент покинул и решение я придумал не столь изящное как хотелось бы. К черту объяснения, как только происходит событие keypress, запихиваем в обработчик тйамаут с минимальным значением паузы, по истечении которого будет вызвана функция форматирования. Та-да!

$(selector).on('keypress', function ( e ) {
	setTimeout(function () {
		$(self).val(format($(self).val()));
	}, 1);
});

Подчищаем и совершенствуем

Добавим пару регэкспов в функцию форматирования. Разрешим пользователям вводить любой разделитель, хоть запятую, хоть точку, но в input будем выводить только правильный разделитель, в моем случае это запятая.  При форматировании функция добавляет пробелы, их нужно исключить из обработки при форматировании, более того, вообще нужны только цифры и разделить. С функцией форматирования разобрались.

Теперь к обработчику нажатия клавиши. Мы можем узнать какие символы пришли от клавиши и какие коды клавиш, поэтому часть ответственности переложим на основной обработчик. О чем идет речь: если пользователь будет вводить что угодно кроме цифр и точки, то вообще не будем запускать форматирование. Это решается не сложно, но есть одна проблема, хочется чтобы стандартное поведение работало. Мне удобнее работать с клавиатуры, навигация по стрелочкам, клавиши home и end, выделение, копирование и вставка, все это должно работать.

И конечно, же не обошлось без проблем на мобильных устройствах. Они, видимо, плохо понимают все это keypress и иже, но есть еще одно замечательное событие для полей ввода, зовется оно «input». Поэтому для мобильников мы еще добавим обработчик на oninput, и к тому же это событие работает и для вставки (ctrl+V), только поддерживается не везде, поэтому оставим обработку и на keypres.

Заканчиваем мусолить. Вот результат работы, обернутый в jquery плагин.

(function ( $ ) {
	$.fn.validMoney = function () {

		var make = function () {
			var self = this;	// перепишем объект

			$(document).ready(function () {
				var def    = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ','],

				    // клавиши, которые не влияют [ctrl, end, home, left, right, tab]
				    access = [17, 35, 36, 37, 39, 9];

				$(self).on('keypress', function ( e ) {
					// is  accessed keys
					if( access.indexOf(e.keyCode) >= 0 ) {
						return true;
					}

					// ctrl+A
					if( e.keyCode === 65 && e.ctrlKey ) {
						return true;
					}

					// backspace and del
					if( e.keyCode === 8 || e.keyCode === 46 ) {
						setTimeout(function () {
							$(self).val(format($(self).val()));
						}, 1);
						return true;
					}

					if( e.ctrlKey ) {
						setTimeout(function () {
							$(self).val(format($(self).val()));
						}, 1);
						return true;
					}

					if( def.indexOf(e.key) >= 0 ) {
						setTimeout(function () {
							$(self).val(format($(self).val()));
						}, 1);
						return true;
					}

					return false;
				});
			});

			self.oninput = function () {
				self.value = format(self.value);

				if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
					setTimeout(function () {
						$(self).val(self.value);
					}, 1);
				}
			};

			function format( val ) {
				var res,
				    int           = '',
				    fraction      = '',
				    has_separator = false;

				// точку меняем на запятую
				val = val.replace(/\./g, ',');

				// оставляем только цифры и запятую
				val = val.replace(/[^0-9,]/g, '');

				// разделим на целую и дробную части
				val = val.split(',');

				// форматирование целой части
				while ( val[0].length > 3 ) {
					int    = val[0].substr(val[0].length - 3) + ' ' + int;
					val[0] = val[0].substr(0, val[0].length - 3);
				}
				int = val[0] + ' ' + int;

				// Убираем последний пробел справа
				res = int.replace(/\s$/g, '');
				// и ноль слева
				res = res.replace(/^0/g, '');

				// форматирование дробной части, при наличии
				if( val.length > 1 ) {
					has_separator = true;
					while ( val[1].length > 3 ) {
						fraction += ' ' + val[1].substr(0, 3);
						val[1] = val[1].substr(3);
					}
					fraction = fraction + ' ' + val[1];

					// сначала удалим пробел
					fraction = fraction.replace(/^\s/g, '');
				}

				if( has_separator ) {
					res += ',';
				}

				if( fraction !== '' ) {
					res += fraction;
				}

				return res;
			}
		};

		return this.each(make);
	};
})(jQuery);

Вызывается как и любой плагин вот так:

$('input').validMoney();

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

Выводы

Есть очень логичный вопрос. Нафига? Зачем мне это было нужно, если хорошо поискать, то можно было бы найти готовый вариант, который бы устроил. Полностью согласен с такой точкой зрения. С маленькой поправкой. Используя готовые решения сторонних разработчиков, есть риск нарваться на какой-то баг, который будет не просто исправить. JQuery плагины сейчас пишутся новичками для новичков, многие разработчики стараются запихать в плагин как можно больше функций, чтобы на выходе получить комбайн. В итоге получается монстр на 5 ног, он может все, но отлаживать его удовольствие ниже среднего. Поэтому некоторые несложные вещи, мне нравится писать самому, плагин, который решает одну задачу. Мне будет потом проще понять, что тут происходит, когда я открою код через N-ное время, а если появится новая фича, то её легко добавить.