I want a PIN entry form where each character is typed into a separate html input.
I am trying to capture keyup events in javascript so that I can change focus to the next input element, saving the user from needing to change it themselves with a mouse click or a tab.
It is working unless the user types really quickly. For example, if the user types the characters '1' and '2' really quickly, I find the first input now correctly has the character '1', while the second input is still empty, and the focus has moved on to the third input.
Why?
Here's the code :
$(document).ready(function () {
$('.pinchar').keyup(function (e) {
if (
(e.which == 8) //backspace
|| (e.which == 46) // del
|| (e.which == 9) // tab
|| (e.which == 13) // return
|| (e.which == 27) // esc
|| (e.which == 37) // arrow
|| (e.which == 38) // arrow
|| (e.which == 39) // arrow
|| (e.which == 40) // arrow
|| (e.which == 27) // esc
|| (e.which == 20) // CAPS LOCK
|| (e.which == 17) // Ctrl
|| (e.which == 18) // Alt
|| (e.which == 16)) { //shift
return false;
} else {
$(this).parent().next().find('.pinchar').focus();
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="enterPINTable">
<tr>
<td>
<input name="txtPIN1" id="txtPIN1" class="pinchar" size="1">
</td>
<td>
<input name="txtPIN2" id="txtPIN2" class="pinchar" size="1">
</td>
<td>
<input name="txtPIN3" id="txtPIN3" class="pinchar" size="1">
</td>
<td>
<input name="txtPIN4" id="txtPIN4" class="pinchar" size="1">
</td>
</tr>
</table>
keyup
will fail when you press a key, and then before releasing it, press another. The order of events is then keydown 1
, keydown 2
, keyup 1
, keydown 2
. But as characters are produced following keydown
(and keypress
) you'll already have two characters in the input
before the first keyup
event in that case.
Instead try the input
event:
$('.pinchar').on('input', function (e) {
// ... etc.
It is triggered immediately on any change.
But to treat other keys correctly, you need to trap the keydown
event as well (for arrow and backspace keys).
I would suggest this code:
$(document).ready(function () {
var $inputs = $(".enterPINTable .pinchar");
function step(inp, dir, clr) {
if (clr) $(inp).val("");
var buffer = $(inp).val(); // get excess characters
// spread characters in next boxes to ensure max 1 char
var curr = $inputs.index(inp);
for (var i = curr, j = 0; j < buffer.length && i < $inputs.length; i++, j++) {
$inputs.eq(i).val(buffer[j]);
}
if (dir < 0) curr-= curr > 0;
else curr = Math.min(curr + (j||dir), $inputs.length-1);
var $next = $inputs.eq(curr);
$next.focus();
setTimeout(function () {
$next.select();
}, 0);
return false;
}
$inputs.on("focus", function () {
$(this).select(); // always select the "whole" (1 char) text
}).on("mouseup", function () { // Attempt to unselect?
setTimeout(function () { // Keep character selected....
$(this).select();
}.bind(this));
}).on("keydown", function (e) {
if (e.key == "ArrowLeft" || e.key == "ArrowUp") return step(this, -1);
if (e.key == "ArrowRight" || e.key == "ArrowDown") return step(this, 1);
if (e.key == "Backspace") return step(this, -1, 1);
}).on("input", function (e) {
step(this, 0);
}).eq(0).focus();
});
.pinchar { width: 1em; text-align: center }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="enterPINTable">
<tr>
<td>
<input name="txtPIN1" id="txtPIN1" class="pinchar">
</td>
<td>
<input name="txtPIN2" id="txtPIN2" class="pinchar">
</td>
<td>
<input name="txtPIN3" id="txtPIN3" class="pinchar">
</td>
<td>
<input name="txtPIN4" id="txtPIN4" class="pinchar">
</td>
</tr>
</table>
The above code aims to always select the current character, so that if you type another character it replaces the current one. If you would succeed to remove the selection (not easy) and try to add a second character, it will end up in the next box. Pasting of a series of characters from the clipboard will also result in a spread of one character per input box.
left
, right
, tab
, shift
+tab
, backspace
, and delete
keys work as could be expected.
Using CSS to size the inputs, and centering the text, seems to give a better result.