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

    По работе недавно столкнулся с, вроде бы, тривиальной задачей — форматирование цены и деление ее по разрядам.
    Ничего сложного решил я. Тем более на просторах интернета лежит уже куча готовых решений от простых и скучных (разворачиваем строку, добавляем через каждые 3 символа пробелы и разворачиваем назад) до вполне интересных (уверен что эту регулярку многие видели, но речь не о ней)
    price.replace(/(\d)(?=(\d\d\d)+([^\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.metaKey === true ||
        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.metaKey === true && code == 86) ||
        (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)
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 30

      +1
      Нет копеек и отрицательных значений. Или я не нашел?
      У себя пока юзаю это
        0
        Копеек и не требовалось
        в вашем плагине при клике в середину курсор переходит в конец текста
          +2
          А еще попробуйте вводить девятки, пока не появится число 10 000 000 000 000 000
        +8
        Запрещать вставлять в поле для ввода или копировать из него, это очень плохо, не делайте так. Сейчас вся работа с выделением смотрится глюком.
          +2
          На следующей итерации менеджеры скажут, что в поле можно ввести любые символы через Alt+Numpad.
          Допустим, вы это поправите как-нибудь.
          Но тогда они скажут, что пользователь может открыть страницу в отладчике, и там вписать в поле что угодно.

          true story
            0
            Полностью согласен, но мой плагин реализовывает/запрещает те функции которые доступны любому пользователю, даже ничего не смыслящему в html.
            +4
            Мне немного ломает мозг движение текста, особенно при стирании символов из середины.
            После добавления
            style="text-align: right;"
            
            к инпуту стало гораздо приятней.
              +1
              Вообщем то да — вы совершенно правы. В реализации на нашем сайте в стилях это прописано.
                +1
                Тоже ломало мозг, когда пытались делать также. Пришли к тому, что форматирование применяется при потере фокуса и снимается при получении.
                Так и возможности форматирования неограничены, например можно приклеить имя валюты, выбранной в дропдауне страницей раньше.
                +8
                Вы своим чудо-велосипедом убили возможность использования буфера обмена в поле ввода. Пожалуйста, не делайте так. Ни в коем случае не урезайте нативную функциональность ради своих плагинов.
                  –5
                  Используется это в аукционах — цена постоянно меняется, и следовательно откуда то копировать и вставлять долго.
                  Да и если просто цена (например в интернет магазине) я не могу представить ни одного случая когда проще скопировать и вставить, нежели ввести самому.
                    +12
                    Не нужно ничего представлять за пользователей, они сами разберутся что как и когда им удобнее использовать. Просто возьмите за правило не трогать нативную функциональность, вы никогда не сделаете так как надо во всех браузерах :)
                  +1
                  Странно работает ваш скрипт, если честно.
                  Открыл я ваш пример.
                  Ввел число. Захотел я его скопировать. Выделил. Зажимаю command и, опа, то, что было выделено пропало.
                  Странный функционал, где бы он не применялся. Я бы подумал, что сайт просто глючит.
                  Сижу я с мака.
                    –1
                    До мака в тестировании так и не дошли. по ctrl+V отлично копирует.
                      0
                      Ctrl + C разумеется ;)
                    +4
                    Вы забыли про event.metaKey — Cmd (вместо Ctrl) на MacOS
                      0
                      Вы правы, спасибо.
                      Обновил.
                        0
                        Вопрос с cmd немного сложнее, чем простое использование event.metaKey. Где-то с год назад искал коды для cmd и наткнулся на ответ на stackoverflow с такими кодами для разных браузеров:

                        Firefox: 224
                        Opera: 17
                        WebKit (Safari/Chrome): 91 (Left Apple) or 93 (Right Apple)

                        В своём проекте ловлю коды 17, 157, 224, 91, 93, 18, но, к сожалению, уже не могу вспомнить, откуда взялись 157 и 18.
                      0
                      Может вам копейки и не нужны но я бы сделал что б была возможность их использовать без этого резко уменьшается возможность применения без допиливания
                        0
                        На Маке с шифтом заглавные буквы (и все остальное тоже) вводятся. Притом, если ввести заглавные буквы и снять фокус с поля – они там и остаются. А если после заглавных ввести цифру – они исчезают.
                          +2
                          Есть один сервис приёма платежей, в котором сделали всё, чтобы пользователю было неудобно вводить номер карты: разделили на четыре инпута и запретили paste. Да ещё и добавили с полдюжины ненужных обязательных полей, которые каждый раз надо заполнять заново.
                            +2
                            Это к тому, что не надо за пользователей решать, как им будет удобнее путём запрета почти всего, к чему они привыкли при редактировании. Поддерживаю то, что не надо блокировать paste и тем более contextmenu.
                            0
                            А вы не пробовали использовать какой-нибудь плагин масок ввода, динамически меняя маску в зависимости от количества символов, чтобы верно расставлять пробелы? Я сам не проверял, будет ли работать, просто это первое, что пришло в голову.
                              0
                              Может быть это не уложилось бы в ТЗ, но я бы просто рядом поставил большими цифрами отформатированную цену и не трогал бы поле ввода.
                              • UFO just landed and posted this here
                                  +1
                                  Тут не согласен. Если оперировать большими числами, например 1000000 — сразу и не скажешь сколько я ввел, придется считать нули или отсчитывать во время ввода, а так сразу понятно это 10 000 или 100 000
                                  0
                                  ИМХО, наиболее адекватное решение тут: github.com/filamentgroup/politespace
                                  Конечно, немного не то, что у вас, но:
                                  — вполне решает задачу (нам ведь нужно показать пользователю отформатированный ввод, верно?)
                                  — не шокирует пользователя, не воюет с системой, не мешает работе с буфером обмена
                                    0
                                    Как уже писалось, разрядные числа лучше вводить справа налево, визуально меньше прыжков. Это правило действительно как для ввода в инпуте так и для вывода скажем в таблице.

                                    Нативные вещи типа копи/паст ни в коем случае нельзя отбрасывать.

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

                                    Резюмируя, мне кажется много кода — толку ноль. Пара тройка регулярок справились бы лучше.
                                      0
                                      Мне кажется, лучше всего было бы не трогать вообще стандартный input, а позволять вводить что угодно и как угодно. Чтобы «глупый пользователь» не запутался, можно в крайнем случае выводить где-нибудь рядом hint, в котором цена будет уже отформатирована («вы ввели 10 135», или даже «вы ввели десять тысяч сто тридцать пять»). Тем более, что в будущем может понадобиться иметь возможность вводить дробные числа, показывать разделитель разрядов запятой, а разделитель дробной части точкой (или наоборот, в зависимости от региональных стандартов) и т.д.
                                        +1
                                        Беда половины таких вот реализаций они совершенно не учитывают особенности языка.

                                        для en-US 1,000,000.00
                                        для ru-RU 1 000 000.00

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

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

                                        Only users with full accounts can post comments. Log in, please.