Форматирование цены, или как я input переписывал

в 14:13, , рубрики: javascript, jquery

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

price.replace(/(d)(?=(ddd)+([^d]|$))/g, '$1 ')

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

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

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

var code = event.keyCode;
if((code < 48 || code > 57) && (code < 96 || code > 105)) {
    event.preventDefault();
    return;
}

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

  • при редактировании элемента в середине курсор перемещается в конец поля (следствие замены значения новым отформатированным)
  • не работают клавиши вперед, назад
  • не работает выделение
  • не работают backspace и delete
  • shift + * тоже не работают, впрочем как и Ctrl

И еще много много чего связанного с нажатиями на кнопки
не такая уж и большая проблема подумал я и добавил в keyup

if (
    code == 9 || // tab
    code == 27 || // ecs
    event.ctrlKey === true || // все что вместе с ctrl
    event.altKey === true || // все что вместе с alt
    event.shiftKey === true || // все что вместе с shift
    (code >= 112 && code <= 123) || // F1 - F12
    (code >= 35 && code <= 39)) // end, home, стрелки
 {
    return;
}

Для отслеживания позиций курсора сделал 2 функции get/set-CursorPosition
и по каждому keyup

    var cursor = $(this).getCursorPosition();
    $(this).val(priceFormatted(value));
    $(this).setCursorPosition(cursor);

Занялся тестированием всего это кода и понял, что не получается отловить событие keyup при нажатии двойных клавиш — например Ctrl + A.
По идее весь текст должен выделяться, но на самом деле происходило следующее. по keydown не происходило ничего (event.ctrlKey === true; return false) и текст выделялся. По keyup текст форматировался заново и выделение сбрасывалось.
В начале я пытался что-то намудрить с проверкой прошлой длины значения и новой, но когда нужно удаление символов (выделил и нажал букву/цифру) все работать отказывалось.
В итоге решено было отказаться от keyup полностью, и перейти полностью на keydown.
Это не предвещало ничего хорошего, потому что я очень сильно сомневался в кроссбраузерности этого решения, да и в целом считывать коды каждой клавиши и добавлять куда нужно символы самому мне не очень хотелось.

Вообщем что из всего этого получилось.

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

    var cursor = $(this).getCursorPosition();
    var code = event.keyCode;
    var startValue = $(this).val();

Вначале нужно определить что за клавиша была нажата

    if ((code >= 48 && code <= 57)) {
        key = (code - 48);
    }
    else if ((code >= 96 && code <= 105 )) {
        key = (code - 96);
    } else {
        return false;
    }

Клавиши с кодом 48 — 57 это верхние цифры 0 — 9, и код 96 — 105 соответствует numpadовским
Если другая клавиша нажата то ничего не делаем.
В место где был курсор вставляем новое значение, форматируем и переставляем курсор.

    var value = startValue.substr(0, cursor) + key + startValue.substring(cursor, startValue.length);
    $(this).val(priceFormatted(value));
    $(this).setCursorPosition(cursor + $(this).val().length - startValue.length);

Неплохо, а что будет если выделить какой-то текст и попробовать написать число? Правильно, текст не удалится и новое число встанет на место старого
При каждом нажатии удалить выделенный текст не составит труда — jquery плагин

$(this).delSelected();

Теперь вернемся к клавишам backspace и delete. Тут все тоже достаточно просто

$(this).val(startValue.substr(0, cursor - 1) + startValue.substring(cursor, startValue.length)); // символ сзади
// или
$(this).val(startValue.substr(0, cursor) + startValue.substring(cursor + 1, startValue.length)); // символ спереди

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

    var delCount = $(this).delSelected();
    if (!delCount) {
        if (startValue[cursor - 1] === ' ') {
            cursor--;
        }
        $(this).val(startValue.substr(0, cursor - 1) + startValue.substring(cursor, startValue.length));
    }
    $(this).val(priceFormatted($(this).val()));
    $(this).setCursorPosition(cursor - (startValue.length - $(this).val().length - delCount));

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

  • Можно вставлять в поле текст
  • Можно перетаскивать туда текст

Надо делать. Запрет на вставку реализации поддался очень легко

if ((event.ctrlKey === true && code == 86) || // Ctrl+V | Shift+insert
    (event.shiftKey === true && code == 45)) 
{
    return false;
}

И запрет на открытие контекстного меню

.bind('contextmenu', function (event) {
    event.preventDefault();
})

Перетаскивание тоже не предвещало особых проблем.

.bind('drop', function (event) {
   // ...

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

event.preventDefault();
// или
return false;

Он оставлял в инпуте второй курсор, который не удалялся никакими способами кроме обновления страницы или консольного

$('...').val(''); // именно пустое

Проблему решил крайне некрасивый кусок кода

.bind('drop', function (event) {
    var value = $(this).val();
    $(this).val(''); // хак для хрома
    // если убрать нижнюю строчку то не работает.
    // курсор удаляется только с удалением и заполнением поля заного.
    $(this).val(value);
    event.preventDefault();
})

Если кто-нибудь сталкивался с такой проблемой и решил ее отпишитесь пожалуйста.

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

Теперь решил поделиться со всеми, потому что в интернете аналогов не обнаружил. Оформил все в виде jquery библиотеки

Потыкать и пощелкать можно тут (jsfiddle)
а скачать — тут (github)

Автор: DOC_tr

Источник

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


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