Search code examples
javascripthtmlvalidationinput

Paste and split long number to multiple input


I am trying to develop a multiple-input form in which I paste a long number that automatically splits into each input by 2 numbers each.

Everything works fine until the user pastes a number of only 5 digits such as 45896 and instead of reporting an error, another number is created in the first input. I can't understand how the form leaves white space, which it then fills by inventing a number, in the first input instead of leaving a blank space on the last input.

In addition I developed else if (input.value.length !== 2 ) input.value = '0' + input.value; to add a 0 in front of the number if the input has only one number but only if the user types in numbers.

I would like the paste function to show an error if one of the inputs contains less than 2 digits and that when pasting a number less than 6 digits long the white space remains on the last input and not on the first.

Is there something wrong in my code or should I improve it to reach my goal?

<style>
.circle input {
  border-radius: 999px;
  float: left;
  max-width: 100px;
  font-size: 2em;
  text-align: center;
  height: 100px;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<form id="tkt_1">
  <div class="circle">
    <input type="tel" min="1" max="99" minlength="2" maxlength="2" oninput="if((this.value.length) > 2) {this.value = this.value.substring(0, 2);}" onchange="handleChange(this);" placeholder="00" />
  </div>
  <div class="circle">
    <input type="tel" min="1" max="99" minlength="2" maxlength="2" oninput="if((this.value.length) > 2) {this.value = this.value.substring(0, 2);}" onchange="handleChange(this);" placeholder="00" />
  </div>
  <div class="circle">
    <input type="tel" min="1" max="99" minlength="2" maxlength="2" oninput="if((this.value.length) > 2) {this.value = this.value.substring(0, 2);}" onchange="handleChange(this);" placeholder="00" />
  </div>
</form>

<script>
// Paste numbers from clipboard in multiple input 
document.addEventListener("paste", function (e) {
    // if the target is a text input
    if (e.target.type === "tel") {
        var data = e.clipboardData.getData('Text');
        // split clipboard text into single characters
        data = data.replace(/[^0-9]/g, "").split(/(?=(?:..)*$)/g);

        // find all other text inputs
        [].forEach.call(document.querySelectorAll("input[type=tel]"), (node, index) => {
            // And set input value to the relative character
            node.value = data[ index ] ?? "";

        });
    }
});

// Validation
function handleChange(input) {
    if (input.value < 0) input.value = "";
    else if (input.value.length !== 2 ) input.value = '0' + input.value;
    else if (input.value < 0.9 || input.value > 99 || input.value.length === 1 || input.value.length === 0) {
        input.style.backgroundColor = 'red';
        return false;
    } else {
        input.style.backgroundColor = '';
        return true;
    }
}

// Input only Numbers 
$('.circle input').keypress(function (event) {
    event = event || window.event;
    var charCode = event.which || event.keyCode;
    var charStr = String.fromCharCode(charCode);
    // FireFox key Del - Supr - Up - Down - Left - Right
    if (event.key !== undefined && event.charCode === 0) {
        return;
    }
    //Only Num
    if (!/^([0-9])*$/.test(charStr)) {
        event.preventDefault();
    }
    //Num and letters
    if (!/^[a-zA-Z0-9]+$/.test(charStr)) {
        event.preventDefault();
    }
});
</script>


Solution

  • The main issue described with the splitting was happening due to the regex split which was leaving the first input single rather than the last input.

    Also, another issue is that onchange is not triggered by pasting values so the handleChange function is never called on pasting the values.

    <style>
    .circle input {
      border-radius: 999px;
      float: left;
      max-width: 100px;
      font-size: 2em;
      text-align: center;
      height: 100px;
    }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <form id="tkt_1">
      <div class="circle">
        <input type="tel" value="00" min="0" minlength="2" maxlength="2" oninput="if((this.value.length) > 2) {this.value = this.value.substring(0, 2);}" onchange="handleChange(this);" />
      </div>
      <div class="circle">
        <input type="tel" value="00" min="0" minlength="2" maxlength="2" oninput="if((this.value.length) > 2) {this.value = this.value.substring(0, 2);}" onchange="handleChange(this);" />
      </div>
      <div class="circle">
        <input type="tel" value="00" min="0" minlength="2" maxlength="2" oninput="if((this.value.length) > 2) {this.value = this.value.substring(0, 2);}" onchange="handleChange(this);" />
      </div>
    </form>
    
    <script>
    // Paste numbers from clipboard in multiple input 
    document.addEventListener("paste", function (e) {
        // if the target is a text input
        if (e.target.type === "tel") {
            var data = e.clipboardData.getData('Text');
            // split clipboard text into single characters
            data = data.replace(/[^0-9]/g, "");
            // Modified approach to split data into pairs
            var pairs = [];
            for (var i = 0; i < data.length; i += 2) {
                pairs.push(data.slice(i, i + 2));
            }
            data = pairs;
    
            // find all other text inputs
            [].forEach.call(document.querySelectorAll("input[type=tel]"), (node, index) => {
                // And set input value to the relative character
                node.value = data[ index ] ?? "";
                handleChange(node); //Added to trigger validation rules for pasted data
    
            });
        }
    });
    
    // Validation
    function handleChange(input) {
        if (input.value < 0) input.value = "";
        else if (input.value.length !== 2 ) input.value = '0' + input.value;
        else if (input.value < 0.9 || input.value > 99 || input.value.length === 1 || input.value.length === 0) {
            input.style.backgroundColor = 'red';
            return false;
        } else {
            input.style.backgroundColor = '';
            return true;
        }
    }
    
    var pasteModifier = false; // Store if control or the meta key is pressed state
    // Need to handle the paste functionality
    $('.circle input').keydown(function (event) {
        if (event.metaKey || event.ctrlKey){
            pasteModifier = true;
        }
    });
    $('.circle input').keyup(function (event) {
        if (event.metaKey || event.ctrlKey){
            pasteModifier = false;
        }
    });
    // Input only Numbers 
    $('.circle input').keypress(function (event) {
        event = event || window.event;
        var charCode = event.which || event.keyCode;
        var charStr = String.fromCharCode(charCode);
        // FireFox key Del - Supr - Up - Down - Left - Right
        if (event.key !== undefined && event.charCode === 0) {
            return;
        }
        // Handle paste command
        if (charCode == 118 && pasteModifier){
          return;
        }
        //Only Num
        else if (!/^([0-9])*$/.test(charStr)) {
            event.preventDefault();
        }
        //Num and letters
        else if (!/^[a-zA-Z0-9]+$/.test(charStr)) {
            event.preventDefault();
        }
    });
    </script>

    I have modified the code to use a regular slicing method to split the data string into 2 digits each and leave a single in the last input if applicable. The handleChange function is also triggered within the loop setting the value for each input so it can act on the pasted data.

    Currently if you paste 5 digits, it does not give an error but appends a 0 at the start of the last input due to the rule set up in the handleChange function.

    If you remove this line

    else if (input.value.length !== 2 ) input.value = '0' + input.value;
    

    you will encounter the red error instead.

    In some browsers including Safari keypress events take higher precedence than the paste commands leading to breaking the paste functionality in this case.

    Also, keypress only sees the visible key triggers, unlike keydown and keyup. So I have included the additional keydown and keyup event handlers to check if the pressed key is the Meta key for Mac or Ctrl key for Windows and keep that stored as a state in the pasteModifier variable.

    This can then be a condition in the keypress handler, where if the Meta or Ctrl key is still pressed (keyup did not fire) and then a V is pressed, it returns as paste handler need not do anything.