[html] Implement an input with a mask

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.

This question is related to html date input mask data-masking

The answer is


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)
}
Date: <input type="text" value="__-__-____" data-mask="__-__-____"/><br/>
Telephone: <input type="text" value="(___) ___-____" data-mask="(___) ___-____"/><br/>

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:

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

(View in JSFiddle)

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


Use this to implement mask:

https://rawgit.com/RobinHerbots/jquery.inputmask/3.x/dist/jquery.inputmask.bundle.js

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


jQuery( '#phn-number[data-inputmask-mask]' ).inputmask();

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

_x000D_
_x000D_
const pattern = "__/__/____";_x000D_
const patternFreeChar = "_";_x000D_
const validDate = [_x000D_
  /^[0-3]$/,_x000D_
  /^(0[1-9]|[12]\d|3[01])$/,_x000D_
  /^(0[1-9]|[12]\d|3[01])[01]$/,_x000D_
  /^((0[1-9]|[12]\d|3[01])(0[13578]|1[02])|(0[1-9]|[12]\d|30)(0[469]|11)|(0[1-9]|[12]\d)02)$/,_x000D_
  /^((0[1-9]|[12]\d|3[01])(0[13578]|1[02])|(0[1-9]|[12]\d|30)(0[469]|11)|(0[1-9]|[12]\d)02)[12]$/,_x000D_
  /^((0[1-9]|[12]\d|3[01])(0[13578]|1[02])|(0[1-9]|[12]\d|30)(0[469]|11)|(0[1-9]|[12]\d)02)(19|20)/_x000D_
]_x000D_
_x000D_
/**_x000D_
 * Validate a date as your type._x000D_
 * @param {string} date The date in format DDMMYYYY as a string representation._x000D_
 * @throws {Error} When the date is invalid._x000D_
 */_x000D_
function validateStartTypingDate(date) {_x000D_
  if ( !date ) return "";_x000D_
  _x000D_
  date = date.substr(0, 8);_x000D_
  _x000D_
  if ( !/^\d+$/.test(date) )_x000D_
   throw new Error("Please type numbers only");_x000D_
  _x000D_
 if ( !validDate[Math.min(date.length-1,validDate.length-1)].test(date) ) {_x000D_
    let errMsg = "";_x000D_
    switch ( date.length ) {_x000D_
     case 1:_x000D_
       throw new Error("Day in month can start only with 0, 1, 2 or 3");_x000D_
        _x000D_
     case 2:_x000D_
       throw new Error("Day in month must be in a range between 01 and 31");_x000D_
        _x000D_
     case 3:_x000D_
       throw new Error("Month can start only with 0 or 1");_x000D_
        _x000D_
     case 4: {_x000D_
       const day = parseInt(date.substr(0,2));_x000D_
       const month = parseInt(date.substr(2,2));_x000D_
        const monthName = new Date(0,month-1).toLocaleString('en-us',{month:'long'});_x000D_
        _x000D_
        if ( month < 1 || month > 12 )_x000D_
         throw new Error("Month number must be in a range between 01 and 12");_x000D_
          _x000D_
        if ( day > 30 && [4,6,9,11].includes(month) )_x000D_
         throw new Error(`${monthName} have maximum 30 days`);_x000D_
          _x000D_
        if ( day > 29 && month === 2 )_x000D_
         throw new Error(`${monthName} have maximum 29 days`);_x000D_
        break; _x000D_
      }_x000D_
         _x000D_
      case 5:_x000D_
      case 6:_x000D_
       throw new Error("We support only years between 1900 and 2099, so the full year can start only with 19 or 20");_x000D_
    }_x000D_
  }_x000D_
  _x000D_
  if ( date.length === 8 ) {_x000D_
   const day = parseInt(date.substr(0,2));_x000D_
    const month = parseInt(date.substr(2,2));_x000D_
    const year = parseInt(date.substr(4,4));_x000D_
    const monthName = new Date(0,month-1).toLocaleString('en-us',{month:'long'});_x000D_
    if ( !isLeap(year) && month === 2 && day === 29 )_x000D_
      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`);_x000D_
  }_x000D_
  _x000D_
  return date;_x000D_
}_x000D_
_x000D_
/**_x000D_
 * Check whether the given year is a leap year._x000D_
 */_x000D_
function isLeap(year) {_x000D_
  return new Date(year, 1, 29).getDate() === 29;_x000D_
}_x000D_
_x000D_
/**_x000D_
 * Move cursor to the end of the provided input element._x000D_
 */_x000D_
function moveCursorToEnd(el) {_x000D_
 if (typeof el.selectionStart == "number") {_x000D_
  el.selectionStart = el.selectionEnd = el.value.length;_x000D_
 } else if (typeof el.createTextRange != "undefined") {_x000D_
  el.focus();_x000D_
  var range = el.createTextRange();_x000D_
  range.collapse(false);_x000D_
  range.select();_x000D_
 }_x000D_
}_x000D_
_x000D_
/**_x000D_
 * Move cursor to the end of the self input element._x000D_
 */_x000D_
function selfMoveCursorToEnd() {_x000D_
 return moveCursorToEnd(this);_x000D_
}_x000D_
_x000D_
const input = document.querySelector("input")_x000D_
_x000D_
input.addEventListener("keydown", function(event){_x000D_
 event.preventDefault();_x000D_
  document.getElementById("date-error-msg").innerText = "";_x000D_
  _x000D_
  // On digit pressed_x000D_
  let inputMemory = this.dataset.inputMemory || "";_x000D_
  _x000D_
  if ( event.key.length === 1 ) {_x000D_
    try {_x000D_
      inputMemory = validateStartTypingDate(inputMemory + event.key);_x000D_
    } catch (err) {_x000D_
      document.getElementById("date-error-msg").innerText = err.message;_x000D_
    }_x000D_
  }_x000D_
  _x000D_
  // On backspace pressed_x000D_
  if ( event.code === "Backspace" ) {_x000D_
   inputMemory = inputMemory.slice(0, -1);_x000D_
  }_x000D_
  _x000D_
  // Build an output using a pattern_x000D_
  if ( this.dataset.inputMemory !== inputMemory ) {_x000D_
   let output = pattern;_x000D_
   for ( let i=0, digit; i<inputMemory.length, digit=inputMemory[i]; i++ ) {_x000D_
     output = output.replace(patternFreeChar, digit);_x000D_
    }_x000D_
    this.dataset.inputMemory = inputMemory;_x000D_
    this.value = output;_x000D_
  }_x000D_
  _x000D_
  // Clean the value if the memory is empty_x000D_
  if ( inputMemory === "" ) {_x000D_
   this.value = "";_x000D_
  }_x000D_
}, false);_x000D_
_x000D_
input.addEventListener('select', selfMoveCursorToEnd, false);_x000D_
input.addEventListener('mousedown', selfMoveCursorToEnd, false);_x000D_
input.addEventListener('mouseup', selfMoveCursorToEnd, false);_x000D_
input.addEventListener('click', selfMoveCursorToEnd, false);
_x000D_
<input type="text" placeholder="DD/MM/YYYY" />_x000D_
<div id="date-error-msg"></div>
_x000D_
_x000D_
_x000D_

A link to jsfiddle: https://jsfiddle.net/d1xbpw8f/56/

Good luck!


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>

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">

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;
}

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=""); });
    }

});   

After reading all post, I did my own implementation, I hope to help to someone:

The idea is,

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

Improvements are welcome.

_x000D_
_x000D_
/**_x000D_
 * charCode [48,57]  Numbers 0 to 9_x000D_
 * keyCode 46     "delete"_x000D_
 * keyCode 9     "tab"_x000D_
 * keyCode 13     "enter"_x000D_
 * keyCode 116    "F5"_x000D_
 * keyCode 8     "backscape"_x000D_
 * keyCode 37,38,39,40 Arrows_x000D_
 * keyCode 10   (LF)_x000D_
 */_x000D_
function validate_int(myEvento) {_x000D_
  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)) {_x000D_
    dato = true;_x000D_
  } else {_x000D_
    dato = false;_x000D_
  }_x000D_
  return dato;_x000D_
}_x000D_
_x000D_
function phone_number_mask() {_x000D_
  var myMask = "(___) ___-____";_x000D_
  var myCaja = document.getElementById("phone");_x000D_
  var myText = "";_x000D_
  var myNumbers = [];_x000D_
  var myOutPut = ""_x000D_
  var theLastPos = 1;_x000D_
  myText = myCaja.value;_x000D_
  //get numbers_x000D_
  for (var i = 0; i < myText.length; i++) {_x000D_
    if (!isNaN(myText.charAt(i)) && myText.charAt(i) != " ") {_x000D_
      myNumbers.push(myText.charAt(i));_x000D_
    }_x000D_
  }_x000D_
  //write over mask_x000D_
  for (var j = 0; j < myMask.length; j++) {_x000D_
    if (myMask.charAt(j) == "_") { //replace "_" by a number _x000D_
      if (myNumbers.length == 0)_x000D_
        myOutPut = myOutPut + myMask.charAt(j);_x000D_
      else {_x000D_
        myOutPut = myOutPut + myNumbers.shift();_x000D_
        theLastPos = j + 1; //set caret position_x000D_
      }_x000D_
    } else {_x000D_
      myOutPut = myOutPut + myMask.charAt(j);_x000D_
    }_x000D_
  }_x000D_
  document.getElementById("phone").value = myOutPut;_x000D_
  document.getElementById("phone").setSelectionRange(theLastPos, theLastPos);_x000D_
}_x000D_
_x000D_
document.getElementById("phone").onkeypress = validate_int;_x000D_
document.getElementById("phone").onkeyup = phone_number_mask;
_x000D_
<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}$">
_x000D_
_x000D_
_x000D_


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.

_x000D_
_x000D_
// This code empowers all input tags having a placeholder and data-slots attribute_x000D_
document.addEventListener('DOMContentLoaded', () => {_x000D_
    for (const el of document.querySelectorAll("[placeholder][data-slots]")) {_x000D_
        const pattern = el.getAttribute("placeholder"),_x000D_
            slots = new Set(el.dataset.slots || "_"),_x000D_
            prev = (j => Array.from(pattern, (c,i) => slots.has(c)? j=i+1: j))(0),_x000D_
            first = [...pattern].findIndex(c => slots.has(c)),_x000D_
            accept = new RegExp(el.dataset.accept || "\\d", "g"),_x000D_
            clean = input => {_x000D_
                input = input.match(accept) || [];_x000D_
                return Array.from(pattern, c =>_x000D_
                    input[0] === c || slots.has(c) ? input.shift() || c : c_x000D_
                );_x000D_
            },_x000D_
            format = () => {_x000D_
                const [i, j] = [el.selectionStart, el.selectionEnd].map(i => {_x000D_
                    i = clean(el.value.slice(0, i)).findIndex(c => slots.has(c));_x000D_
                    return i<0? prev[prev.length-1]: back? prev[i-1] || first: i;_x000D_
                });_x000D_
                el.value = clean(el.value).join``;_x000D_
                el.setSelectionRange(i, j);_x000D_
                back = false;_x000D_
            };_x000D_
        let back = false;_x000D_
        el.addEventListener("keydown", (e) => back = e.key === "Backspace");_x000D_
        el.addEventListener("input", format);_x000D_
        el.addEventListener("focus", format);_x000D_
        el.addEventListener("blur", () => el.value === pattern && (el.value=""));_x000D_
    }_x000D_
});
_x000D_
[data-slots] { font-family: monospace }
_x000D_
<label>Date time: _x000D_
    <input placeholder="dd/mm/yyyy hh:mm" data-slots="dmyh">_x000D_
</label><br>_x000D_
<label>Telephone:_x000D_
    <input placeholder="+1 (___) ___-____" data-slots="_">_x000D_
</label><br>_x000D_
<label>MAC Address:_x000D_
    <input placeholder="XX:XX:XX:XX:XX:XX" data-slots="X" data-accept="[\dA-H]">_x000D_
</label><br>_x000D_
<label>Signed number (3 digits):_x000D_
    <input placeholder="±___" data-slots="±_" data-accept="^[+-]|(?!^)\d" size="4">_x000D_
</label><br>_x000D_
<label>Alphanumeric:_x000D_
    <input placeholder="__-__-__-____" data-slots="_" data-accept="\w" size="13">_x000D_
</label><br>
_x000D_
_x000D_
_x000D_


Examples related to html

Embed ruby within URL : Middleman Blog Please help me convert this script to a simple image slider Generating a list of pages (not posts) without the index file Why there is this "clear" class before footer? Is it possible to change the content HTML5 alert messages? Getting all files in directory with ajax DevTools failed to load SourceMap: Could not load content for chrome-extension How to set width of mat-table column in angular? How to open a link in new tab using angular? ERROR Error: Uncaught (in promise), Cannot match any routes. URL Segment

Examples related to date

How do I format {{$timestamp}} as MM/DD/YYYY in Postman? iOS Swift - Get the Current Local Time and Date Timestamp Typescript Date Type? how to convert current date to YYYY-MM-DD format with angular 2 SQL Server date format yyyymmdd Date to milliseconds and back to date in Swift Check if date is a valid one change the date format in laravel view page Moment js get first and last day of current month How can I convert a date into an integer?

Examples related to input

Angular 4 - get input value React - clearing an input value after form submit Min and max value of input in angular4 application Disable Button in Angular 2 Angular2 - Input Field To Accept Only Numbers How to validate white spaces/empty spaces? [Angular 2] Can't bind to 'ngModel' since it isn't a known property of 'input' Mask for an Input to allow phone numbers? File upload from <input type="file"> Why does the html input with type "number" allow the letter 'e' to be entered in the field?

Examples related to mask

Update row values where certain condition is met in pandas Implement an input with a mask Phone mask with jQuery and Masked Input Plugin OpenCV - Apply mask to a color image How to Apply Mask to Image in OpenCV?

Examples related to data-masking

Implement an input with a mask