Search code examples
javascriptangularjsinput-filter

How do I make a filtered input able to have edits occur inside it, not at the end?


On a project I am working on, my team has recently developed a 'masked' input in JavaScript/Angular that works as a directive that relies upon a filter, in order to present a phone number to a user as a U.S. formatted phone number. Because our app will only be used in the United States, we don't have to worry about making it flexible to other national phone formats.

For those unfamiliar this is the usual format of a U.S. telephone number:

(AAA) EEE-TTTT xXXXXX

Where:

  • A: Area Code
  • E: Exchange Code
  • T: Terminal Code
  • X: Extension

The problem that I originally set out to solve, was that attempts to edit the Area Code would cause the cursor to jump to the end of the phone number. The deeper problem is that, due to our current implementation of this directive, attempting to insert any information in the middle of the phone number causes the cursor to jump to the end!

Here's how it's implemented and used:

Markup:

<input type="text"
       id="some-phone-number"
       name="somePhoneNumber"
       class="form-control"
       data-ng-model="vm.somePhoneNumber" 
       phone-input />

Directive:

angular.module('app').directive('phoneInput', [
    '$filter', '$browser', phoneInputDir
]);

function phoneInputDir($filter, $browser) {
    return {
        require: 'ngModel',
        link: function($scope, $element, $attrs, ngModelCtrl) {
            var listener = function() {
                var value = $element.val().replace(/[^0-9]/g, '');
                $element.val($filter('tel')(value, false));
            };

            ngModelCtrl.$formatters.unshift(function (modelValue) {
                return $filter('tel')(modelValue, false);
            });

            // This runs when we update the text field
            ngModelCtrl.$parsers.push(function(viewValue) {
                return viewValue.replace(/[^0-9]/g, '').slice(0, 15);
            });

            // This runs when the model gets updated on the scope directly and keeps our view in sync
            ngModelCtrl.$render = function() {
                $element.val($filter('tel')(ngModelCtrl.$viewValue, false));
            };

            $element.bind('change', listener);
            $element.bind('keydown', function(event) {
                var key = event.keyCode;
                // If the keys include the CTRL, SHIFT, ALT, or META keys, or the arrow keys, do nothing.
                // This lets us support copy and paste too
                if (key == 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) {
                    return;
                }
                $browser.defer(listener); // Have to do this or changes don't get picked up properly
            });

            $element.bind('paste cut', function() {
                $browser.defer(listener);
            });
        }
    };
}    

Filter:

angular.module('app').filter('tel', function() {
    return function(input) {
        console.log(input);
        if (!input) {
            return '';
        }

        var value = input.toString().trim().replace(/^\+/, '');

        if (value.match(/[^0-9]/)) {
            return input;
        }

        // Phone number format: 
        // (AAA) EEE-TTTT xXXXXX...
        var areaCode = value.slice(0, 3),
            exchangeCode = value.slice(3, 6),
            terminalCode = value.slice(6, 10),
            extension = value.slice(10);

        var result = '';
        if (areaCode)
            result += '(' + areaCode + ') ';
        if (exchangeCode)
            result += exchangeCode;
        if (terminalCode)
            result += '-' + terminalCode;
        if (extension)
            result += ' x' + extension;

        return result;
    };
});

Question: In what way can I change the directive/filter here to be able to make inline edits, instead of any keypress causing the cursor to shift to the end of the input?


Solution

  • Since you are updating the text in the input on the fly, you have to store the cursor position before you update and then set it after you update. You'll also need to add one to the cursor position to account for the characters you are inserting. To accomplish this you can modify your listener function in your directive as such:

    var listener = function() {
        var cursorPos = $element[0].selectionStart + 1;
        var value = $element.val().replace(/[^0-9]/g, '');
        $element.val($filter('tel')(value, false));
        $element[0].setSelectionRange(cursorPos, cursorPos);
    };
    

    Since $element is a selector and not the actual HTML element itself, you need to reference the first child (which is the HTML element) and this is why $element[0] has to be used.