Implement an input with a mask
Solution 1:
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:
- jquery.inputmask
- MASKED INPUT PLUGIN
- Politespace (presents an alternative to input masks)
Solution 2:
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>
Solution 3:
I did my own implementation using the following principles:
- allow entry of only numbers. (keypress event)
- get all numbers in an array
- 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}$">