78

I would like to implement a mask for a text input field which accepts a date. The masked value should display directly inside of the input.

Something like this:

<input type='text' value='____/__/__'>

I wrote the mask as a value in that example, but my intent is to allow people to write a date without typing / or - to separate months, years and days. The user should be able to enter numbers into the displayed field, while the mask enforces the format automatically as the user types.

I have seen this behavior on other sites, but I have no idea how it works or how to implement it myself.

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
Stanislas Piotrowski
  • 2,595
  • 10
  • 40
  • 59
  • I had the same issues but saw some interesting stuff and wanted to share. I customized mine to feature my country code at `+237 ---,---,---`. That white spaces are just a series of underscore(`_`). If you country numbers are `+1 777 888 990` just write `+1 ___,___,___`(3 underscores joint). http://nishangsystem.com/nishang_stuff/ – Ni shang Jul 02 '18 at 14:18

9 Answers9

72

A solution that responds to the input event instead of key events (like keyup) will give a smooth experience (no wiggles), and also works when changes are made without the keyboard (context menu, mouse drag, other device...).

The code below will look for input elements that have both a placeholder attribute and a data-slots attribute. The latter should define the character(s) in the placeholder that is/are intended as input slot, for example, "_". An optional data-accept attribute can be provided with a regular expression that defines which characters are allowed in such a slot. The default is \d, i.e. digits.

// This code empowers all input tags having a placeholder and data-slots attribute
document.addEventListener('DOMContentLoaded', () => {
    for (const el of document.querySelectorAll("[placeholder][data-slots]")) {
        const pattern = el.getAttribute("placeholder"),
            slots = new Set(el.dataset.slots || "_"),
            prev = (j => Array.from(pattern, (c,i) => slots.has(c)? j=i+1: j))(0),
            first = [...pattern].findIndex(c => slots.has(c)),
            accept = new RegExp(el.dataset.accept || "\\d", "g"),
            clean = input => {
                input = input.match(accept) || [];
                return Array.from(pattern, c =>
                    input[0] === c || slots.has(c) ? input.shift() || c : c
                );
            },
            format = () => {
                const [i, j] = [el.selectionStart, el.selectionEnd].map(i => {
                    i = clean(el.value.slice(0, i)).findIndex(c => slots.has(c));
                    return i<0? prev[prev.length-1]: back? prev[i-1] || first: i;
                });
                el.value = clean(el.value).join``;
                el.setSelectionRange(i, j);
                back = false;
            };
        let back = false;
        el.addEventListener("keydown", (e) => back = e.key === "Backspace");
        el.addEventListener("input", format);
        el.addEventListener("focus", format);
        el.addEventListener("blur", () => el.value === pattern && (el.value=""));
    }
});
[data-slots] { font-family: monospace }
<label>Date time: 
    <input placeholder="dd/mm/yyyy hh:mm" data-slots="dmyh">
</label><br>
<label>Telephone:
    <input placeholder="+1 (___) ___-____" data-slots="_">
</label><br>
<label>MAC Address:
    <input placeholder="XX:XX:XX:XX:XX:XX" data-slots="X" data-accept="[\dA-H]">
</label><br>
<label>Alphanumeric:
    <input placeholder="__-__-__-____" data-slots="_" data-accept="\w" size="13">
</label><br>
<label>Credit Card:
    <input placeholder=".... .... .... ...." data-slots="." data-accept="\d" size="19">
</label><br>
trincot
  • 317,000
  • 35
  • 244
  • 286
  • 4
    Besides being really smooth and not interfering with caret movement with arrows, Home and End, this solution behaves as one would expect when typing with the caret in the middle of the string. – andref Mar 03 '20 at 20:21
  • 2
    I love it, wish I could buy you a coffee. +1, anyways! – Sablefoste Feb 03 '22 at 17:12
  • This approach fails if you try to edit what was entered e.g. `01/12/2022` and try to change the month from `12` to `01` – Drenai Jul 16 '22 at 19:04
  • 1
    @Drenai, I wouldn't call this "fails". The guiding principle here is that you edit *one* series of characters, and so if you put the cursor in the middle, the input characters are *inserted* into that series, moving existing characters to the right. With this way of working, you have to delete the (two) characters you want to replace, and then type the new characters. Then it will be fine. – trincot Jul 16 '22 at 19:09
  • If I try that, and delete two digits, and enter '9', it enters that value twice. Probably just needs a bit of tweaking – Drenai Jul 16 '22 at 19:15
  • What do you mean? I don't see any issue with typing 9: it just inserts one 9 when I type one 9, and another if I type another. NB: this is not supposed to do date *validation* if that is what you mean. – trincot Jul 16 '22 at 19:21
  • Maybe it's a mobile browser issue - Chrome Android. If I delete two month digits, it adds the next digit I enter twice – Drenai Jul 16 '22 at 19:38
  • Strange, I cannot reproduce this. – trincot Jul 16 '22 at 19:49
  • 1
    @trincot thank you, this is a great solution! I have it implemented in my code and it's working well. Can you please help me figure out how to apply the formatting to existing data? I'm using this in a phone number input field in a modal and in some cases the user will already have a phone number stored and just need to edit it. When I focus in the field, the formatting appears but is there an event that I can add to format the field when the modal loads? please and thank you again :) – Christine Treacy Sep 13 '22 at 17:59
  • That will depend how the modal is made. If it is a window object, it will have its own DOMContentLoaded event. But if you cannot make it work, maybe ask a new question, and provide the code you have tried with. – trincot Sep 13 '22 at 18:54
  • @trincot Just figured it out actually, thanks for your reply. – Christine Treacy Sep 13 '22 at 19:28
  • 1
    @trincot, thank you for this solution ! It works great. However, I've discovered that on safari iOS when using your solution, the 'change' event is not triggered on the fields affected by your code. Cannot figure why. Do you have a hint ? Thanks! – pl4n3th Mar 30 '23 at 10:31
  • Changes made to input values by code never fire a `change` event. Or do you mean something else? – trincot Mar 30 '23 at 10:32
44

Input masks can be implemented using a combination of the keyup event, and the HTMLInputElement value, selectionStart, and selectionEnd properties. Here's a very simple implementation which does some of what you want. It's certainly not perfect, but works well enough to demonstrate the principle:

Array.prototype.forEach.call(document.body.querySelectorAll("*[data-mask]"), applyDataMask);

function applyDataMask(field) {
    var mask = field.dataset.mask.split('');
    
    // For now, this just strips everything that's not a number
    function stripMask(maskedData) {
        function isDigit(char) {
            return /\d/.test(char);
        }
        return maskedData.split('').filter(isDigit);
    }
    
    // Replace `_` characters with characters from `data`
    function applyMask(data) {
        return mask.map(function(char) {
            if (char != '_') return char;
            if (data.length == 0) return char;
            return data.shift();
        }).join('')
    }
    
    function reapplyMask(data) {
        return applyMask(stripMask(data));
    }
    
    function changed() {   
        var oldStart = field.selectionStart;
        var oldEnd = field.selectionEnd;
        
        field.value = reapplyMask(field.value);
        
        field.selectionStart = oldStart;
        field.selectionEnd = oldEnd;
    }
    
    field.addEventListener('click', changed)
    field.addEventListener('keyup', changed)
}
ISO Date: <input type="text" value="____-__-__" data-mask="____-__-__"/><br/>
Telephone: <input type="text" value="(___) ___-____" data-mask="(___) ___-____"/><br/>

(View in JSFiddle)

There are also a number of libraries out there which perform this function. Some examples include:

Ajedi32
  • 45,670
  • 22
  • 127
  • 172
  • 6
    The cursor shifts to left when it goes over some char in the mask (e.g. hyphen or bracket) – Andree Sep 02 '15 at 11:05
  • 1
    @Andree Correct, that's one of the reasons why I said "it's certainly not perfect, but works well enough to demonstrate the principle". A real-world implementation would need to be more complicated in order to account for edge cases like that which degrade the user experience. – Ajedi32 Sep 03 '15 at 14:19
  • Is there a reason to use `keyup` instead of the `keydown` event? – fbynite Mar 20 '16 at 20:48
  • @fbynite I don't really remember now, but I think it was because they keydown event is fired before the content of the text box is even updated. So you can use keydown if you want, but you'll then have to use `event.preventDefault()` and update the contents of the box with the script itself. For a simple example like the one in my answer I thought that was a bit complex, so I used keyup instead. – Ajedi32 Mar 20 '16 at 21:06
  • For any integration on production environment - I recommend using `jquery.inputmask` that you referenced. This is not a trivial task & it's much better to use a dedicated solution for this than tailoring something by yourself. – BornToCode Jul 07 '20 at 15:05
  • Another library is [jQuery Mask Plugin](https://github.com/igorescobar/jQuery-Mask-Plugin). A WordPress plugin that embeds a minimized version of the jQuery Mask Plugin is [Masks Form Fields](https://github.com/igorescobar/jQuery-Mask-Plugin). It enables the JS, and creates classes, so it can easily be added to a Contact Form 7 input field. A CDN link and sample script tag for the jQuery Mask Plugin is listed [here](https://cdnjs.com/libraries/jquery.mask). – SherylHohman Oct 05 '20 at 22:04
  • Another one on github, which has great instructions on how to use it. However, like many other code samples out there, nay edits the user makes to the input gets added to the END of the input, not where the cursor is. [Input Masking](https://github.com/estelle/input-masking) by estelle. Hence, I recommend jQuery Mask Plugin, from my previous comment. UX is as expected - seamless for the user. – SherylHohman Oct 05 '20 at 22:20
22

I did my own implementation using the following principles:

  1. allow entry of only numbers. (keypress event)
  2. get all numbers in an array
  3. replace every "_" character of the mask by a number from the array in a loop

Improvements are welcome.

/**
 * charCode [48,57]     Numbers 0 to 9
 * keyCode 46           "delete"
 * keyCode 9            "tab"
 * keyCode 13           "enter"
 * keyCode 116          "F5"
 * keyCode 8            "backscape"
 * keyCode 37,38,39,40  Arrows
 * keyCode 10           (LF)
 */
function validate_int(myEvento) {
  if ((myEvento.charCode >= 48 && myEvento.charCode <= 57) || myEvento.keyCode == 9 || myEvento.keyCode == 10 || myEvento.keyCode == 13 || myEvento.keyCode == 8 || myEvento.keyCode == 116 || myEvento.keyCode == 46 || (myEvento.keyCode <= 40 && myEvento.keyCode >= 37)) {
    dato = true;
  } else {
    dato = false;
  }
  return dato;
}

function phone_number_mask() {
  var myMask = "(___) ___-____";
  var myCaja = document.getElementById("phone");
  var myText = "";
  var myNumbers = [];
  var myOutPut = ""
  var theLastPos = 1;
  myText = myCaja.value;
  //get numbers
  for (var i = 0; i < myText.length; i++) {
    if (!isNaN(myText.charAt(i)) && myText.charAt(i) != " ") {
      myNumbers.push(myText.charAt(i));
    }
  }
  //write over mask
  for (var j = 0; j < myMask.length; j++) {
    if (myMask.charAt(j) == "_") { //replace "_" by a number 
      if (myNumbers.length == 0)
        myOutPut = myOutPut + myMask.charAt(j);
      else {
        myOutPut = myOutPut + myNumbers.shift();
        theLastPos = j + 1; //set caret position
      }
    } else {
      myOutPut = myOutPut + myMask.charAt(j);
    }
  }
  document.getElementById("phone").value = myOutPut;
  document.getElementById("phone").setSelectionRange(theLastPos, theLastPos);
}

document.getElementById("phone").onkeypress = validate_int;
document.getElementById("phone").onkeyup = phone_number_mask;
<input type="text" name="phone" id="phone" placeholder="(123) 456-7890" required="required" title="e.g (123) 456-7890" pattern="^\([0-9]{3}\)\s[0-9]{3}-[0-9]{4}$">
trincot
  • 317,000
  • 35
  • 244
  • 286
Rodney Salcedo
  • 1,228
  • 19
  • 23
  • I will make the function name generic and use similar logic that Ajedi32 used and get the mask from the "data-mask" attribute. This way you are not only masking for phone number but any type that is supplied in the data-mask attribute – programmerboy May 29 '17 at 19:01
  • I want to post number without masking. is it possible – Chetan Dec 01 '18 at 09:43
  • @Sam you could store the number in another input (could be hidden), parse the the masked value before submit event or better parse the masked value on the server side – Rodney Salcedo Dec 05 '18 at 15:37
  • It is a pity that you cannot move the cursor with the keyboard -- it always flips back to the end. On the other hand, if I paste some text by clicking the context menu "Paste", no validation is done. – trincot Dec 15 '22 at 12:30
12

You can achieve this also by using JavaScripts's native method. Its pretty simple and doesn't require any extra library to import.

<input type="text" name="date" placeholder="yyyy-mm-dd" onkeyup="
  var date = this.value;
  if (date.match(/^\d{4}$/) !== null) {
     this.value = date + '-';
  } else if (date.match(/^\d{4}\-\d{2}$/) !== null) {
     this.value = date + '-';
  }" maxlength="10">
shubhamkes
  • 516
  • 5
  • 12
7

You can also try my implementation, which doesn't have delay after each key press when typing the contents, and has full support for backspace and delete.

You can try it online: https://jsfiddle.net/qmyo6a1h/1/

    <html>
    <style>
    input{
      font-family:'monospace';
    }
    </style>
    <body>
      <input type="text" id="phone" placeholder="123-5678-1234" title="123-5678-1234" input-mask="___-____-____">
      <input type="button" onClick="showValue_phone()" value="Show Value" />
      <input type="text" id="console_phone" />
      <script>
        function InputMask(element) {
          var self = this;

          self.element = element;

          self.mask = element.attributes["input-mask"].nodeValue;

          self.inputBuffer = "";

          self.cursorPosition = 0;

          self.bufferCursorPosition = 0;

          self.dataLength = getDataLength();

          function getDataLength() {
            var ret = 0;

            for (var i = 0; i < self.mask.length; i++) {
              if (self.mask.charAt(i) == "_") {
                ret++;
              }
            }

            return ret;
          }

          self.keyEventHandler = function (obj) {
            obj.preventDefault();

            self.updateBuffer(obj);
            self.manageCursor(obj);
            self.render();
            self.moveCursor();
          }

          self.updateBufferPosition = function () {
            var selectionStart = self.element.selectionStart;
            self.bufferCursorPosition = self.displayPosToBufferPos(selectionStart);
            console.log("self.bufferCursorPosition==" + self.bufferCursorPosition);
          }

          self.onClick = function () {
            self.updateBufferPosition();
          }

          self.updateBuffer = function (obj) {
            if (obj.keyCode == 8) {
              self.inputBuffer = self.inputBuffer.substring(0, self.bufferCursorPosition - 1) + self.inputBuffer.substring(self.bufferCursorPosition);
            }
            else if (obj.keyCode == 46) {
              self.inputBuffer = self.inputBuffer.substring(0, self.bufferCursorPosition) + self.inputBuffer.substring(self.bufferCursorPosition + 1);
            }
            else if (obj.keyCode >= 37 && obj.keyCode <= 40) {
              //do nothing on cursor keys.
            }
            else {
              var selectionStart = self.element.selectionStart;
              var bufferCursorPosition = self.displayPosToBufferPos(selectionStart);
              self.inputBuffer = self.inputBuffer.substring(0, bufferCursorPosition) + String.fromCharCode(obj.which) + self.inputBuffer.substring(bufferCursorPosition);
              if (self.inputBuffer.length > self.dataLength) {
                self.inputBuffer = self.inputBuffer.substring(0, self.dataLength);
              }
            }
          }

          self.manageCursor = function (obj) {
            console.log(obj.keyCode);
            if (obj.keyCode == 8) {
              self.bufferCursorPosition--;
            }
            else if (obj.keyCode == 46) {
              //do nothing on delete key.
            }
            else if (obj.keyCode >= 37 && obj.keyCode <= 40) {
              if (obj.keyCode == 37) {
                self.bufferCursorPosition--;
              }
              else if (obj.keyCode == 39) {
                self.bufferCursorPosition++;
              }
            }
            else {
              var bufferCursorPosition = self.displayPosToBufferPos(self.element.selectionStart);
              self.bufferCursorPosition = bufferCursorPosition + 1;
            }
          }

          self.setCursorByBuffer = function (bufferCursorPosition) {
            var displayCursorPos = self.bufferPosToDisplayPos(bufferCursorPosition);
            self.element.setSelectionRange(displayCursorPos, displayCursorPos);
          }

          self.moveCursor = function () {
            self.setCursorByBuffer(self.bufferCursorPosition);
          }

          self.render = function () {
            var bufferCopy = self.inputBuffer;
            var ret = {
              muskifiedValue: ""
            };

            var lastChar = 0;

            for (var i = 0; i < self.mask.length; i++) {
              if (self.mask.charAt(i) == "_" &&
                bufferCopy) {
                ret.muskifiedValue += bufferCopy.charAt(0);
                bufferCopy = bufferCopy.substr(1);
                lastChar = i;
              }
              else {
                ret.muskifiedValue += self.mask.charAt(i);
              }
            }

            self.element.value = ret.muskifiedValue;

          }

          self.preceedingMaskCharCount = function (displayCursorPos) {
            var lastCharIndex = 0;
            var ret = 0;

            for (var i = 0; i < self.element.value.length; i++) {
              if (self.element.value.charAt(i) == "_"
                || i > displayCursorPos - 1) {
                lastCharIndex = i;
                break;
              }
            }

            if (self.mask.charAt(lastCharIndex - 1) != "_") {
              var i = lastCharIndex - 1;
              while (self.mask.charAt(i) != "_") {
                i--;
                if (i < 0) break;
                ret++;
              }
            }

            return ret;
          }

          self.leadingMaskCharCount = function (displayIndex) {
            var ret = 0;

            for (var i = displayIndex; i >= 0; i--) {
              if (i >= self.mask.length) {
                continue;
              }
              if (self.mask.charAt(i) != "_") {
                ret++;
              }
            }

            return ret;
          }

          self.bufferPosToDisplayPos = function (bufferIndex) {
            var offset = 0;
            var indexInBuffer = 0;

            for (var i = 0; i < self.mask.length; i++) {
              if (indexInBuffer > bufferIndex) {
                break;
              }

              if (self.mask.charAt(i) != "_") {
                offset++;
                continue;
              }

              indexInBuffer++;
            }
            var ret = bufferIndex + offset;

            return ret;
          }

          self.displayPosToBufferPos = function (displayIndex) {
            var offset = 0;
            var indexInBuffer = 0;

            for (var i = 0; i < self.mask.length && i <= displayIndex; i++) {
              if (indexInBuffer >= self.inputBuffer.length) {
                break;
              }

              if (self.mask.charAt(i) != "_") {
                offset++;
                continue;
              }

              indexInBuffer++;
            }

            return displayIndex - offset;
          }

          self.getValue = function () {
            return this.inputBuffer;
          }
          self.element.onkeypress = self.keyEventHandler;
          self.element.onclick = self.onClick;
        }

        function InputMaskManager() {
          var self = this;

          self.instances = {};

          self.add = function (id) {
            var elem = document.getElementById(id);
            var maskInstance = new InputMask(elem);
            self.instances[id] = maskInstance;
          }

          self.getValue = function (id) {
            return self.instances[id].getValue();
          }

          document.onkeydown = function (obj) {
            if (obj.target.attributes["input-mask"]) {
              if (obj.keyCode == 8 ||
                obj.keyCode == 46 ||
                (obj.keyCode >= 37 && obj.keyCode <= 40)) {

                if (obj.keyCode == 8 || obj.keyCode == 46) {
                  obj.preventDefault();
                }

                //needs to broadcast to all instances here:
                var keys = Object.keys(self.instances);
                for (var i = 0; i < keys.length; i++) {
                  if (self.instances[keys[i]].element.id == obj.target.id) {
                    self.instances[keys[i]].keyEventHandler(obj);
                  }
                }
              }
            }
          }
        }

        //Initialize an instance of InputMaskManager and
        //add masker instances by passing in the DOM ids
        //of each HTML counterpart.
        var maskMgr = new InputMaskManager();
        maskMgr.add("phone");

        function showValue_phone() {
          //-------------------------------------------------------__Value_Here_____
          document.getElementById("console_phone").value = maskMgr.getValue("phone");
        }
      </script>
    </body>

    </html>
Cecilk Cao
  • 71
  • 1
  • 2
  • 1
    Love it! Nice job – Protozoid Jun 24 '18 at 12:43
  • It seems impossible to select text using the keyboard (shift + arrow), also the caret behaves strange when I press left-arrow when already at the extreme left: it flips to the extreme right and pressing left-arrow more doesn't have an effect. If I paste some text by clicking the context menu "Paste", no validation is done. – trincot Dec 15 '22 at 12:35
3

Use this to implement mask:

https://cdnjs.cloudflare.com/ajax/libs/jquery.inputmask/5.0.6/jquery.inputmask.min.js

<input id="phone_number" class="ant-input" type="text" placeholder="(XXX) XXX-XXXX" data-inputmask-mask="(999) 999-9999">



jQuery("#phone_number").inputmask({"mask": "(999) 999-9999"});
ravina vala
  • 119
  • 4
0

Below i describe my method. I set event on input in input, to call Masking() method, which will return an formatted string of that we insert in input.

Html:

<input name="phone" pattern="+373 __ ___ ___" class="masked" required>

JQ: Here we set event on input:

$('.masked').on('input', function () {
    var input = $(this);
    input.val(Masking(input.val(), input.attr('pattern')));
});

JS: Function, which will format string by pattern;

function Masking (value, pattern) {
var out = '';
var space = ' ';
var any = '_';

for (var i = 0, j = 0; j < value.length; i++, j++) {
    if (value[j] === pattern[i]) {
        out += value[j];
    }
    else if(pattern[i] === any && value[j] !== space) {
        out += value[j];
    }
    else if(pattern[i] === space && value[j] !== space) {
        out += space;
        j--;
    }
    else if(pattern[i] !== any && pattern[i] !== space) {
        out += pattern[i];
        j--;
    }
}

return out;
}
Dumitru Boaghi
  • 183
  • 2
  • 4
0

I wrote a similar solution some time ago.
Of course it's just a PoC and can be improved further.

This solution covers the following features:

  • Seamless character input
  • Pattern customization
  • Live validation while you typing
  • Full date validation (including correct days in each month and a leap year consideration)
  • Descriptive errors, so the user will understand what is going on while he is unable to type a character
  • Fix cursor position and prevent selections
  • Show placeholder if the value is empty

const patternFreeChar = "_";
const dayValidator = [/^[0-3]$/, /^0[1-9]|[12]\d|3[01]$/];
const monthValidator = [/^[01]$/, /^0[1-9]|1[012]$/];
const yearValidator = [/^[12]$/, /^19|20$/, /^(19|20)\d$/, /^(19|20)\d\d$/];

/**
 * Validate a date as your type.
 * @param {string} date The date in the provided format as a string representation.
 * @param {string} format The format to use.
 * @throws {Error} When the date is invalid.
 */
function validateStartTypingDate(date, format='DDMMYYYY') {
  if ( !date ) return "";

  date = date.substr(0, 8);

  if ( !/^\d+$/.test(date) )
    throw new Error("Please type numbers only");

  const formatAsArray = format.split('');
  const dayIndex = formatAsArray.findIndex(c => c == 'D');
  const monthIndex = formatAsArray.findIndex(c => c == 'M');
  const yearIndex = formatAsArray.findIndex(c => c == 'Y');

  const dayStr = date.substr(dayIndex,2);
  const monthStr = date.substr(monthIndex,2);
  const yearStr = date.substr(yearIndex,4);

  if ( dayStr && !dayValidator[dayStr.length-1].test(dayStr) ) {
    switch (dayStr.length) {
      case 1:
        throw new Error("Day in month can start only with 0, 1, 2 or 3");
      case 2:
        throw new Error("Day in month must be in a range between 01 and 31");
    }
  }

  if ( monthStr && !monthValidator[monthStr.length-1].test(monthStr) ) {
    switch (monthStr.length) {
      case 1:
        throw new Error("Month can start only with 0 or 1");
      case 2:
        throw new Error("Month number must be in a range between 01 and 12");
    }
  }

  if ( yearStr && !yearValidator[yearStr.length-1].test(yearStr) ) {
    switch (yearStr.length) {
      case 1:
        throw new Error("We support only years between 1900 and 2099, so the full year can start only with 1 or 2");
      default:
        throw new Error("We support only years between 1900 and 2099, so the full year can start only with 19 or 20");
    }
  }

  const day = parseInt(dayStr);
  const month = parseInt(monthStr);
  const year = parseInt(yearStr);
  const monthName = new Date(0,month-1).toLocaleString('en-us',{month:'long'});

  if ( day > 30 && [4,6,9,11].includes(month) )
    throw new Error(`${monthName} have maximum 30 days`);

  if ( day > 29 && month === 2 )
    throw new Error(`${monthName} have maximum 29 days`);

  if ( date.length === 8 ) {
    if ( !isLeap(year) && month === 2 && day === 29 )
      throw new Error(`The year you are trying to enter (${year}) is not a leap year. Thus, in this year, ${monthName} can have maximum 28 days`);
  }

  return date;
}

/**
 * Check whether the given year is a leap year.
 */
function isLeap(year) {
  return new Date(year, 1, 29).getDate() === 29;
}

/**
 * Move cursor to the end of the provided input element.
 */
function moveCursorToEnd(el) {
  if (typeof el.selectionStart == "number") {
    el.selectionStart = el.selectionEnd = el.value.length;
  } else if (typeof el.createTextRange != "undefined") {
    el.focus();
    var range = el.createTextRange();
    range.collapse(false);
    range.select();
  }
}

/**
 * Move cursor to the end of the self input element.
 */
function selfMoveCursorToEnd() {
  return moveCursorToEnd(this);
}

const inputs = document.querySelectorAll("input");

inputs.forEach(input => {
  const { format, pattern } = input.dataset;
  input.addEventListener("keydown", function(event){
    event.preventDefault();
    document.getElementById("date-error-msg").innerText = "";

    // On digit pressed
    let inputMemory = this.dataset.inputMemory || "";

    if ( event.key.length === 1 ) {
      try {
        inputMemory = validateStartTypingDate(inputMemory + event.key, format);
      } catch (err) {
        document.getElementById("date-error-msg").innerText = err.message;
      }
    }

    // On backspace pressed
    if ( event.code === "Backspace" ) {
      inputMemory = inputMemory.slice(0, -1);
    }

    // Build an output using a pattern
    if ( this.dataset.inputMemory !== inputMemory ) {
      let output = pattern;
      for ( let i=0, digit; i<inputMemory.length, digit=inputMemory[i]; i++ ) {
        output = output.replace(patternFreeChar, digit);
      }
      this.dataset.inputMemory = inputMemory;
      this.value = output;
    }

    // Clean the value if the memory is empty
    if ( inputMemory === "" ) {
      this.value = "";
    }
  }, false);

  input.addEventListener('select', selfMoveCursorToEnd, false);
  input.addEventListener('mousedown', selfMoveCursorToEnd, false);
  input.addEventListener('mouseup', selfMoveCursorToEnd, false);
  input.addEventListener('click', selfMoveCursorToEnd, false);
});
input {
  width: 250px;
}
<div><input type="text" placeholder="DD/MM/YYYY" data-format="DDMMYYYY" data-pattern="__/__/____" /></div>
<div><input type="text" placeholder="MM/DD/YYYY" data-format="MMDDYYYY" data-pattern="__/__/____" /></div>
<div><input type="text" placeholder="YYYY-MM-DD" data-format="YYYYMMDD" data-pattern="____-__-__" /></div>
<div><input type="text" placeholder="Day: DD, Year: YYYY, Month: MM" data-format="DDYYYYMM" data-pattern="Day: __, Year: ____, Month: __" /></div>
<div id="date-error-msg"></div>

A link to jsfiddle: https://jsfiddle.net/sm3xw61n/2/

Good luck!

Slavik Meltser
  • 9,712
  • 3
  • 47
  • 48
0

I taken from this thread decision Implement an input with a mask and adapted it for IE10, and added setter- and getter- functions.

BUT I TESTED FOR PHONE-mask ONLY

$(document).ready(function(){
    var el_arr = document.querySelectorAll("[placeholder][data-slots]");
    for (var el_ind=0; el_ind < el_arr.length; el_ind++ ){
        var el = el_arr[el_ind];
        var pattern = el.getAttribute("placeholder"),
            slots = new Set(el.getAttribute("data-slots") || "_"),
            prev = function(j){return Array.from(pattern, function(c,i){ return slots.has(c)? j=i+1: j;});}(0),
            first = pattern.split('').findIndex(function(c){return slots.has(c);} ),
            accept = new RegExp(el.getAttribute("data-accept") || "\\d", "g"),
            clean = function(input){input = input.match(accept) || [];return Array.from(pattern, function(c){return input[0] === c || slots.has(c) ? input.shift() || c : c;});},
            format = function(){
                var elem = this;
                var i_j_arr = [el.selectionStart, el.selectionEnd].map(function(i){
                    i = clean(el.value.slice(0, i)).findIndex(function(c){ return slots.has(c);});
                    return i<0? prev[prev.length-1]: elem.back? prev[i-1] || first: i;
                });
                el.value = clean(el.value).join('');
                el.setSelectionRange(i_j_arr[0], i_j_arr[1]);
                this.back = false;
            },
            // sdo added
            get_masked_value = function(){
                var input = this.value;
                var ret=[];
                for(var k in pattern){
                    if ( !input[k] )break;
                    if( slots.has(pattern[k]) && input[k]!=pattern[k]){
                        ret.push(input[k]);
                    } 
                } 
                return ret.join('');
            },
            set_masked_value = function(input){
                var ret=[];
                var index_in_value = 0;
                for(var k in pattern){
                    if( slots.has(pattern[k]) && input[index_in_value]){
                        ret.push(input[index_in_value]);
                        index_in_value++;
                    }
                    else{
                        ret.push(pattern[k]);
                    }
                } 
                this.value = ret.join('');
            }                    
        ;
        el.get_masked_value = get_masked_value;
        el.set_masked_value = set_masked_value;
        el.back = false;
        el.addEventListener("keydown", function(event){ this.back = event.key === "Backspace";});
        el.addEventListener("input", format);
        el.addEventListener("focus", format);
        el.addEventListener("blur", function() { return el.value === pattern && (el.value=""); });
    }

});   
Solo.dmitry
  • 690
  • 8
  • 15