How do I keep track of character positions, deletions, insertions, and substitutions in an input field?

Viewed 94

I am creating a date formatter.

On input (input event),it will format the input into the correct format as the user types, for example, 11/22/2020.

There are certain conditions to be met.

  • The first character (month first character must either be a 0 or 1)
  • If the first character in the month is a 1, then the second month character must be either 0,1, or 2
  • The delimiters ("/") are automatically inserted as the user types
  • The first character of the day must either be 0,1,2, or 3
  • All characters except the delimiters must be number characters

The issue comes into play when the user removes a character.

How do I keep track of the positions of the characters upon input? Also, how do I keep track of the positions of the characters that are deleted or replaced?

7 Answers

you can try this the inputElement.value to get the value and then check that if the length of the value is more than two then add the /. something like this

var x = inputElement.value;
x = x + '/';

and remove them when it is less than two similarly you can also check that first number of the month is a 0 or 1 or 2 and if its not then you can forcefully change that

if (charCode == 8 || (charCode > 47 && charCode < 58))

this check will make sure that the value of the input is number only well that's all i can help.i think you can figure the rest on your own and rap your code with try and catch to handle errs

This will tell you the character position of the most recent keypress. You could use it as a base for further calculation.

inputElement.addEventListener("input", e => {
    console.log(e.target.selectionStart)
}

I have added working example here

The regex can be used to validate the date format (MM/DD/YYYY). The regex is ^(1[0-2]|0[1-9])/(3[01]|[12][0-9]|0[1-9])/[0-9]{4}$.

To get the cursor position, e.target.selectionStart can be used.

const allowedSlashIndices = [2, 5];
let prevValue = '';
let currentValue = '';

function onChange(e) {
  const text = e.target.value;
  regex = new RegExp('^(1[0-2]|0[1-9])/(3[01]|[12][0-9]|0[1-9])/[0-9]{4}$');
  if (!regex.test(text)) {
    document.getElementById('error').innerHTML = 'Invalid date';
  } else {
    document.getElementById('error').innerHTML = '';
  }
}
function onInput(e) {
  prevValue = currentValue;
  currentValue = e.target.value;
  const nextIndex = e.target.value.length;
  if (allowedSlashIndices.includes(nextIndex) && prevValue !== currentValue) {
    e.target.value = `${e.target.value}/`;
  }
}

function onKeydown(e) {
  if (isNumberOrSlash(e)) {
    if (isSlash(e)) {
      const cursorIndex = e.target.selectionStart;
      if (!allowedSlashIndices.includes(cursorIndex)) {
        e.preventDefault();
      }
    }
  } else {
    // Do not allow keys other than number
    e.preventDefault();
  }
}

function isNumberOrSlash(evt) {
  evt = evt ? evt : window.event;
  const charCode = evt.which ? evt.which : evt.keyCode;
  const isNotNumber = charCode > 31 && (charCode < 48 || charCode > 57);
  const arrowKeys = [37, 38, 39, 40].includes(charCode);

  const isNotSlash = charCode !== 191;
  if (isNotNumber && isNotSlash && !arrowKeys) {
    return false;
  }

  return true;
}

function isSlash(evt) {
  evt = evt ? evt : window.event;
  const charCode = evt.which ? evt.which : evt.keyCode;
  return charCode === 191;
}
<input type="text" onchange="return onChange(event)" onkeydown="return onKeydown(event)" oninput="return onInput(event)" placeholder="MM/DD/YYYY"/>  
<div style="color:red"  id="error"></div>

I have done a bit of testing in JSFiddle (here's my fiddle).

function checkDate(val) {
    let pat = /^(0[1-9]|1[012])[/](0[1-9]|[12][0-9]|3[01])[/](19|20)\d\d$/
    if (pat.test(val)) {
        document.getElementById('evals').innerHTML = "Hello?"
    }
    else {
        document.getElementById('evals').innerHTML = "Goodbye!"
    }
}
<form>
    <input oninput="checkDate(this.value)"
           pattern="^(0[1-9]|1[012])[/](0[1-9]|[12][0-9]|3[01])[/](19|20)\d\d$"
           required>
    <input type="submit">
</form>
<div id="evals"></div>

I know there is a way to do it, I just can't seem to get it completely. In my fiddle it will only show if the entire thing is formatted correctly, not while the user is typing it in.

One way you could do it, it by putting regular expressions into an array, and check each character that it put in based on the regular expression for that spot. If it doesn't put in the character.

Like Vibhav said, try using type="date". I know it's not as cool, but it will work better... and will be more robust.

//the variable t has the user input and its length is the cursor position
//code begin
var t=""
var x= document.getElementById('asdf')
x.onkeydown=function(ev){
  ev.preventDefault()
  if(ev.key.length==1){t+=ev.key}
  else if(ev.key=="Backspace"){
    t=t.split``.filter((a,i)=>{return i!=t.length-1}).join``
  }
  x.value=t; setCaretText(x.id,t.length)
  console.log(getCaretIndex(x)) //in case you wanted not to control where the user's cursor is but still get the cursor position
}
//code end













//random helper functions cursor stuff
function setCaretDom(ell, n) {//set for non-input tags
    var el = document.getElementById(ell);
    var range = document.createRange();
    var sel = window.getSelection();
    range.setStart(el.childNodes[0], n)
    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
    el.focus();
}
function setCaretText(elemId, caretPos) {//set for input tags
    var elem = document.getElementById(elemId);
    if(elem!=null) {
      if(elem.createTextRange) {
        var range = elem.createTextRange();
        range.move('character', caretPos);
        range.select();
      }
      else {
        elem.focus();
        elem.setSelectionRange(caretPos, caretPos);
      }
    }
}
function getCaretIndex(inputField) {//set for input tags
  const startPos = inputField.selectionStart;
  const endPos = inputField.selectionEnd;
  const dir = inputField.selectionDirection;

  if (startPos === endPos) {
    return startPos;
  }
  if (dir === "forward") {
    return endPos;
  } else {
    return startPos;
  }
}
<input id="asdf" placeholder="mm/dd/yyyy"/>

Okay, so I just did a little more testing in Codepen this time, and this is what I got:

<form>
    <input oninput="checkDate(this)" pattern="^(0[1-9]|1[012])[/](0[1-9]|[12][0-9]|3[01])[/](19|20)\d\d$" required>
    <input type="submit">
</form>
var prevVal = ""
var checkDate = (e) => {
    let ev = e.value
    if (/^\d+$/.test(ev.substring(e.value.length-1))) {
        if ((ev.length>10) || (ev.length < prevVal.length
        && (ev.length==2 || ev.length==5)))
            e.value = ev.substring(0,ev.length-1)
        else if (ev.length==2 || ev.length==5)
            e.value+="/"
        prevVal = ev
    }
    else if (ev.length > 0 && ev.substring(ev.length-1)!=="/")
        e.value = prevVal
    if (ev.length == 0)
        prevVal = ""
}

It isn't complete, but so far, it will only all the user to enter in digits and will automatically place the "/" and it won't let you type any more than what is needed for "mm/dd/yyyy".
The only problems are that once a "/" in inserted you can't backspace immediately, you have to enter more digits and then backspace.
It also doesn't stop the user from entering a value of, say, "55/55/5555"...

Just build off of this to get it to how you want...
I will be doing a little more testing to see if I can finally get it to work completely...

Hope this helps and good luck!

Success!
NOTE: This code meets all these conditions:

  • The first character (month first character must either be a 0 or 1)
  • If the first character in the month is a 1, then the second month character must be either 0,1, or 2
  • The delimiters ("/") are automatically inserted as the user types
  • The first character of the day must either be 0,1,2, or 3
  • All characters except the delimiters must be number characters

I just realized that your actual question was how to keep track of the positions of the characters... in my solution, I just use String.length to figure out where the character inputted is...

I finally found a good solution that isn't too long and that is all in one function (except for one variable...):
Here it is on Codepen.

Here is the code:

var prev = ""
var checkDate = (el) => {
    let ev = el.value
    let stop = false
    
    if (prev.length < ev.length) {
        switch (prev.length) {
            case 0:
                if (parseInt(ev[0]) > 1)
                    stop = true
            break;
            case 1:
                if (parseInt(ev[0]+ev[1]) > 12)
                    stop = true
            break;
            case 3:
                if (parseInt(ev[3]) > 3)
                    stop = true
            break;
            case 4:
                if (parseInt(ev[3]+ev[4]) > 31)
                    stop = true
            break;
        }
        
        if (stop)
            el.value = ev.substring(0,ev.length-1)
        else if (!(/^\d+$/.test(ev.substring(ev.length-1))) || ev.length > 10)
            el.value = ev.substring(0,ev.length-1)
        else if (ev.length == 2 || ev.length == 5)
            el.value += "/"
        else if (ev.length>2 && ev.indexOf("/") != 2)
            el.value = ev.substring(0,2)+"/"+ev.substring(2,ev.length)
        else if (ev.length>5 && ev.indexOf("/",3) != 5)
            el.value = ev.substring(0,5)+"/"+ev.substring(5,ev.length)
    }
    prev = el.value
}
<form>
    <input oninput="checkDate(this)" required>
    <input type="submit">
</form>

Let me know if there is anything that you want me to clarify! I will be happy to help!

Related