Phone Textbox (tel, telephone)

As part of a new UI library, I have created a custom server control that creates the markup for a simple textbox using the new HTML5 input type of "tel" and the pattern attribute. The Phone Textbox supports the following features:

  • Native browser support, if available.
  • Ability to set the Country ISO 3166-1-alpha-2 code to specify the needed validation.
  • Instant character validation on keyup and blur.
  • Optional phone extension.
  • Optional toll-free phone number formatting (e.g. 1-800-555-5555).
  • Regular expression validation upon submit. The "pattern" attribute is set dynamically according to the Country ISO Code.
  • Dynamic maxlength assignment.
  • Fully integrates with the UI Library - Ajax Form feature.
  • UI Manifesto compliance:

The Demo

Regular and toll-free number with optional extension.
Regular and toll-free number, but extension not allowed.
Regular number with optional extension, but no toll-free formatting.

The Markup

Just a basic input element, with a normal type attribute of "tel" and some custom data (data-*) attributes to allow feature customization. Once the Script stuff is in place on a website, any page can take advantage of this Phone Textbox by simply setting an input's type attribute to "tel" and setting the desired optional custom data attributes. If native browser support is available, then the browser implementation of the control will be used in addtion to the onkey up and pattern validation.

That sounds like a DRY solution to me (see UI Manifesto).

 
<div class="form legendLook horizontalFormLayout">
    <div id="hdivFormTest1ConfirmationMessage" class="confirmationMessageWrapper hide"></div>
 
    <label for="txtPhone1">Phone #1:</label>
    <input type="tel" id="txtPhone1" style="width:500px;"
        required 
        placeholder="Regular and toll-free number with optional extension."
        data-countryIsoCode="US" 
        data-allowExtension="true"
        data-useTollFreeFormatting="true"            
        data-formGroup="formAuthenticate" 
        name="Value" />

    <label for="txtPhone2">Phone #2:</label>
    <input type="tel" id="txtPhone2" style="width:500px;"
        required 
        placeholder="Regular and toll-free number, but extension not allowed."  
        data-countryIsoCode="US" 
        data-allowExtension="false"
        data-useTollFreeFormatting="true"          
        data-formGroup="formAuthenticate" 
        name="_" />

    <label for="txtPhone3">Phone #3:</label>
    <input type="tel" id="txtPhone3" style="width:500px;"
        required 
        placeholder="Regular number with optional extension, but no toll-free formatting."  
        data-countryIsoCode="US" 
        data-allowExtension="true"
        data-useTollFreeFormatting="false"          
        data-formGroup="formAuthenticate" 
        name="_" />
            
    <div class="formButtonWrapper clearFix">
        <input type="submit" class="ajaxFormPost buttonStrong" value="Submit"
            data-formGroup="formAuthenticate" 
            data-serviceRequestUrl="..." 
            data-successFunction="successFormTest1"
            data-confirmActionMessage=""
            data-processingMessage=""
            data-overlayWindowWhileProcessing="true" />
    </div>
</div>

The Script

function formatPhoneNumber(value, countryIsoCode, allowExtension, useTollFreeFormatting, event) {
    var requestingElement = $("#" + event.target.id);

    //8  = Backspace
    //9  = Tab (so will stay highlighted)
    //16 = Reverse Tab - Shift+Tab (so will stay highlighted)
    if (getKeycode(event) != 8 && getKeycode(event) != 9 && getKeycode(event) != 16) {
        var unformattedPhoneNumber = makeNumeric(value, false, null, null);
        var isValidTollFreePrefix = false;
        var formattedPhoneNumber = "";

        switch (countryIsoCode.toUpperCase()) {
            default:
                //Build and assign the "pattern", if not already there.
                if (!isAttrDefined(requestingElement.attr("pattern")) || requestingElement.attr("pattern").length == 0) {
                    //(801) 123-1234               - /^\((\d{3})\) (\d{3})-(\d{4})$/
                    //(801) 123-1234 ext. 12345    - /^\((\d{3})\) (\d{3})-(\d{4}) ext. \d+$/
                    //1-800-123-1234               - /^1-(800|888|877|866|855|844|833|822)-(\d{3})-(\d{4})$/
                    //1-800-123-1234 ext. 12345    - /^1-(800|888|877|866|855|844|833|822)-(\d{3})-(\d{4}) ext. \d+$/
                    //all combined                 - /^((\((\d{3})\) (\d{3})-(\d{4}))|(\((\d{3})\) (\d{3})-(\d{4}) ext. \d+)|(1-(800|888|877|866|855|844|833|822)-(\d{3})-(\d{4}))|(1-(800|888|877|866|855|844|833|822)-(\d{3})-(\d{4}) ext. \d+))$/
                    var patternValue = "/^((\\((\\d{3})\\) (\\d{3})-(\\d{4}))|(\\((\\d{3})\\) (\\d{3})-(\\d{4}) ext. \\d+)|(1-(800|888|877|866|855|844|833|822)-(\\d{3})-(\\d{4}))|(1-(800|888|877|866|855|844|833|822)-(\\d{3})-(\\d{4}) ext. \\d+))$/";
                    requestingElement.attr("pattern", patternValue);
                }

                //Assign the "maxlength", if not already there.
                if (!isAttrDefined(requestingElement.attr("maxlength")) || requestingElement.attr("maxlength").length == 0) {
                    if (allowExtension) {
                        requestingElement.attr("maxlength", "30");
                    }
                    else {
                        requestingElement.attr("maxlength", "14");
                    }
                }

                //valid current & future toll free number prefixes : http://en.wikipedia.org/wiki/Toll_free_number
                var validTollFreePrefixies = new Array("800", "888", "877", "866", "855", "844", "833", "822");
                var validTollFreePrefixiesFirst2Only = new Array("80", "88", "87", "86", "85", "84", "83", "82");
                var first3Digits = unformattedPhoneNumber.charAt(0) + unformattedPhoneNumber.charAt(1) + unformattedPhoneNumber.charAt(2);
                var first4Digits = first3Digits + unformattedPhoneNumber.charAt(3);

                for (pfx in validTollFreePrefixies) {
                    if (validTollFreePrefixies[pfx] == first3Digits) {
                        isValidTollFreePrefix = true;
                        unformattedPhoneNumber = "1" + unformattedPhoneNumber;
                    }
                    else if ("1" + validTollFreePrefixies[pfx] == first4Digits) {
                        isValidTollFreePrefix = true;
                    }
                    else if ("1" + validTollFreePrefixiesFirst2Only[pfx] == first3Digits && unformattedPhoneNumber.length == 3) {
                        isValidTollFreePrefix = true;
                    }
                }

                if (!useTollFreeFormatting) {
                    isValidTollFreePrefix = false;
                }

                if (isValidTollFreePrefix) {
                    //1-###-###-#### ext. ####
                    formattedPhoneNumber += "1" + "-" + unformattedPhoneNumber.charAt(1) + unformattedPhoneNumber.charAt(2) + unformattedPhoneNumber.charAt(3);
                    if (unformattedPhoneNumber.length > 3) {
                        formattedPhoneNumber += "-";
                        formattedPhoneNumber += unformattedPhoneNumber.charAt(4) + unformattedPhoneNumber.charAt(5) + unformattedPhoneNumber.charAt(6);
                        if (unformattedPhoneNumber.length > 6) {
                            formattedPhoneNumber += "-";
                            formattedPhoneNumber += unformattedPhoneNumber.charAt(7) + unformattedPhoneNumber.charAt(8) + unformattedPhoneNumber.charAt(9) + unformattedPhoneNumber.charAt(10);
                            if (allowExtension) {
                                if (unformattedPhoneNumber.length > 11) {
                                    formattedPhoneNumber += " ext. ";
                                    for (var upn = 11; upn <= (unformattedPhoneNumber.length - 1); upn++) { //the (<= & -1) is needed to not get messed up with Packer 3.1 compression
                                        formattedPhoneNumber += unformattedPhoneNumber.charAt(upn);
                                    }
                                }
                            }
                        }
                    }
                }
                else {
                    //If less then 4 in length, this still could be a valid toll-free number
                    if (unformattedPhoneNumber.length > 2) {
                        if (unformattedPhoneNumber.charAt(0) == "1") {
                            var tempUnformattedPhoneNumber = "";
                            for (var upn2 = 1; upn2 <= (unformattedPhoneNumber.length - 1); upn2++) { //the (<= & -1) is needed to not get messed up with Packer 3.1 compression
                                tempUnformattedPhoneNumber += unformattedPhoneNumber.charAt(upn2);
                            }
                            unformattedPhoneNumber = tempUnformattedPhoneNumber;
                        }
                        //(###) ###-#### ext. ####
                        if (unformattedPhoneNumber.length > 2 || value.charAt(0) == "(") {
                            formattedPhoneNumber += "(";
                        }
                        formattedPhoneNumber += unformattedPhoneNumber.charAt(0) + unformattedPhoneNumber.charAt(1) + unformattedPhoneNumber.charAt(2);
                        if (unformattedPhoneNumber.length > 2) {
                            formattedPhoneNumber += ") ";
                            formattedPhoneNumber += unformattedPhoneNumber.charAt(3) + unformattedPhoneNumber.charAt(4) + unformattedPhoneNumber.charAt(5);
                            if (unformattedPhoneNumber.length > 5) {
                                formattedPhoneNumber += "-";
                                formattedPhoneNumber += unformattedPhoneNumber.charAt(6) + unformattedPhoneNumber.charAt(7) + unformattedPhoneNumber.charAt(8) + unformattedPhoneNumber.charAt(9);
                                if (allowExtension) {
                                    if (unformattedPhoneNumber.length > 10) {
                                        formattedPhoneNumber += " ext. ";
                                        for (var upn3 = 10; upn3 <= (unformattedPhoneNumber.length - 1); upn3++) { //the (<= & -1) is needed to not get messed up with Packer 3.1 compression
                                            formattedPhoneNumber += unformattedPhoneNumber.charAt(upn3);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    else {
                        if (value == "1" && useTollFreeFormatting) {
                            formattedPhoneNumber = "1-";
                        }
                        else if (value == "1--" && useTollFreeFormatting) {
                            formattedPhoneNumber = "1-";
                        }
                        else if (value == "1-8" && useTollFreeFormatting) {
                            formattedPhoneNumber = "1-8";
                        }
                        else if (value.charAt(0) == "(") {
                            formattedPhoneNumber = "(" + unformattedPhoneNumber;
                        }
                        else {
                            formattedPhoneNumber = unformattedPhoneNumber;
                        }
                    }
                }
                break;
        }

        return formattedPhoneNumber;
    }
    else {
        return value;
    }
}

//Enable Phone Textbox(es)
$(parentElementSelector + " input[type='tel']").each(function () {
    $(this).keyup(function (event) {
        var countryIsoCode = ((!isAttrDefined($(this).attr("data-countryIsoCode")) || $(this).attr("data-countryIsoCode").length == 0) ? $("#hfGlobalCountryIsoCode").val() : $(this).attr("data-countryIsoCode"));
        var allowExtension = (($(this).attr("data-allowExtension") == "true") ? true : false);
        var useTollFreeFormatting = (($(this).attr("data-useTollFreeFormatting") == "true") ? true : false);
        $(this).val(formatPhoneNumber($(this).val(), countryIsoCode, allowExtension, useTollFreeFormatting, event));
    });
});

The CSS

 
    Not applicable.