- Test stand {#test-stand}
- Key event properties
- Processing the character:
keypress - Cancelling user input
- Working with scan-codes:
keydown/keyup - Tasks and examples
Keyboard events is one of wilder parts of frontend development. There are inconsistencies and cross-browser problems.
But still there are recipes which help to cope with ordinary situations easily.
There are following keyboard events:
keydown- A key is pressed down.
keypress- A character key is pressed.
keyup- A key is released.
There is a fundamental difference between keypress and keydown.
Keydowntriggers on any key press and gives scan-code.Keypresstriggers afterkeydownand gives char-code, but it is guaranteed for character keys only.
Test stand {#test-stand}
To better understand keyboards event, we’ll use the test stand.
document.getElementById('kinput').onkeydown = khandle
document.getElementById('kinput').onkeyup = khandle
document.getElementById('kinput').onkeypress = khandle
function khandle(e) {
e = e || event
if (document.forms.keyform[e.type + 'Ignore'].checked) return
var evt = e.type
while (evt.length < 10) evt += ' '
showmesg(evt +
' keyCode=' + e.keyCode +
' which=' + e.which +
' charCode=' + e.charCode +
' char=' + String.fromCharCode(e.keyCode || e.charCode) +
(e.shiftKey ? ' +shift' : '') +
(e.ctrlKey ? ' +ctrl' : '') +
(e.altKey ? ' +alt' : '') +
(e.metaKey ? ' +meta' : ''), 'key'
)
if (document.forms.keyform[e.type + 'Stop'].checked) {
e.preventDefault ? e.preventDefault() : (e.returnValue = false)
}
}
In the test stand, char = String.fromCharCode(e.keyCode || e.charCode).
Unknown properties and methods are explained in details below.
- Try pressing down a character key, like ‘S’, ‘1’ or ‘,’.
It triggerskeydownand thenkeypress. When the key is released, thekeyupoccurs. - Try pressing a special key like ‘Shift’, ‘Delete’ or ‘Arrow Up’.
It triggerskeydownand thenkeyupon the time of releasing.
Firefox and Opera trigger keypress for most special keys.
IE also triggers keypress for Esc.
It is possible that a special key results in keypress, but generally browsers don’t trigger it (and they shouldn’t).
The rule:
keydown/keyupare for any keys.keypressis for characters.
Key event properties
There was a heck crazy zoo in keyboard events few years ago. Now we live iin happy time. Most terrible bugs and inconsistencies are fixed in recent browsers. IE is also pleasant to deal with. There are just several tricks to use.
Keyboard event have the following specific properties:
keyCode- The scan-code of the key. For example, if an “a” key is pressed, the character can be “a” or “A” (or a character from another language), but the
keyCodeis same. It depends on key only, not on the resulting character.You can always check the code by clicking on the test stand… There are two main tables: Mozilla and IE. They are almost equal, but differ in few keys:
';', '='and'-'.You can read a great article from John Walter: JavaScript Madness: Keyboard Events. It contains both events information and code tables.
charCode- The character code, ASCII.
which- A non-standard property, the hybrid of
charCodeandkeyCode, with the sole purpose to confuse a developer.
But in the zoo of key events it also plays a good role. Particulary, it helps to get the character. That’s described further in the section. shiftKey, ctrlKey, altKey, metaKey- The properties are boolean and reflect the state of corresponding key: Shift, Ctrl, Alt or Command(Mac only).
Processing the character: keypress
The latest DOM 3 Events specification deprecates keypress and replaces it by textInput.
But as of now, textInput event is supported in Safari/Chrome only, and the support is incomplete, so this is a far future.
The keypress is now.
The only event which reliably provides the character is keypress.
In all browsers except IE, the charCode property is defined for keypress and contains the character code. Opera follows this principle, but bugs on special keys. It triggers keypress without charCode on some of them, e.g “backspace”.
Internet Explorer has it’s own way. In case of keypress event it doesn’t set the charCode, but puts the character code in keyCode instead of scan-code.
So here’s the function to get all a symbol from keypress event:
// event.type must be keypress
function getChar(event) {
if (event.which == null) {
return String.fromCharCode(event.keyCode) // IE
} else if (event.which!=0 && event.charCode!=0) {
return String.fromCharCode(event.which) // the rest
} else {
return null // special key
}
}
Note the last case. Special keys have no symbols. Applying
String.fromCharCode to special keys gives weird results.
We filter them with event.which!=0 && event.charCode!=0 check. It guarantees that the key is not special even in older browsers and Opera.
You can also find the following function in the net:
function getChar(event) {
return String.fromCharCode(event.keyCode || event.charCode)
}
It works wrong for many special keys for the reason described above. For example, it returns character ‘&’ when ‘Arrow Up’ is pressed.
Cancelling user input
A non-special key usually results in a character. This can be prevented.
For all browsers except Opera, two events can be used to cancel a key input: keydown and keypress. But Opera is more picky. It will cancel character only if preventDefault comes from keypress.
Try to type something in the input below: <input *!*onkeydown="return false"*/!* type="text" size="30"> <input *!*onkeypress="return false"*/!* type="text" size="30">
Try to type something in the input below:
In the example above, there will be no characters in both inputs for all browsers with exception of Opera which ignores preventDefault on keydown and hence will show keys on the first input.
In IE and Safari/Chrome preventing default action on keydown cancels keypressed event too. Try that on the test stand: prevent keydown and type something. There should be no keypressed in IE and Safari/Chrome.
Demo: char-uppercasing input
The following input uppercases all characters:
<input id='my' type="text">
<script>
document.getElementById('my').onkeypress = function(event) {
var char = getChar(event || window.event)
if (!char) return // special key
this.value = char.toUpperCase()
return false
}
</script>
Characters which you type here will be upper-cased:
In the example above, the default action is prevented by return false, our own value is added instead.
There is a problem with this widget. If you move the cursor in the middle of input and type something - it appends to the end. So, the uppercasing input is not so simple, but still doable, because there exists a way to get caret position.
Write an input which accepts only digits. There is a demo below.
We need characters here, so the event is keypress.
The algorithm is to take the char and see if it is numeric. Cancel default action if it’s not.
The only minor pitfall is to accept special chars as well as numbers. Otherwise it will become impossible to use arrow keys and delete in browsers which generate keypress on them. Namely, Firefox.
So, here’s the solution:
input.onkeypress = function(e) {
e = e || event
var chr = getChar(e)
if (!isNumeric(chr) && chr !== null) {
return false
}
}
Helper function getChar gets the character, isNumeric checks for number.
The full solution code is here: tutorial/browser/events/numeric-input/index.html.
Working with scan-codes: keydown/keyup
Sometimes, we need to know only a key, not the character. For example, special like arrows, page up, page down, enter, escape - there are no characters at all.
Most browsers do not generate keypress for such keys. Instead, for special keys the keydown/keyup should be used.
The good news is that modern browsers and (even older) IE agree on keycodes for almost all special keys (with the exception of branded keys like IE start button).
Another example is hotkeys. When we implement a custom hotkey with JavaScript, it should work same no matter of case and current language. We don’t want a character. We just want a scan code.
Scan codes VS char codes
As we know, the char code is a unicode character code. It is given only in the keypress event.
A scan-code is given on keydown/keyup.
For all alphanumeric and most special keys, the scan code generally equals a character code. In case of a letter, the scan code equals an uppercased english letter char code.
For example, you want to track “Ctrl-S” hotkey. The checking code for keydown event would be:
e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)
And it doesn’t matter if user the resulting char is “s” or “S” or another language letter.
For alphanumeric keys, the scan code equals the character code of the uppercased english letter/digit.
The scan code do not equal the char code for most punctuation characters including brackets and arithmetic symbols.
For example, a “-” key has keyCode=109 in Firefox, keyCode=189 in IE, but it’s charCode=45. Obviously no match.
For all keys except ';', '=' and '-' different browsers use same key code.
Try that on the test stand above. Type ‘-’ and watch keydown keyCodekeypress charCode.
Special actions
Some special actions can be prevented. If the backspace is pressed, but the keydown returns false, the character will not be deleted.
But of course certain actions can’t be cancelled, especially OS-level ones. Alt+F4 closes browser window in most operating systems, no matter what you do in JavaScript.
Tasks and examples
Click on the mouse below. Then press arrow keys, it will move.
The task is a prototype of the real keyboard navigation on an interface.
The source document and mousie await you here: tutorial/browser/events/mousie-src/index.html.
The getOffset for absolute coords is attached too, if you need it.
The algorithm
There are few steps which comprise the solution:
- How to track when a mousie is in clicked state and when it’s not?
- An obvious solution here is
focus. Key events will trigger on the focused element and bubble up.To make
DIVfocusable, we should addtabindex:
<div style="width:41px;height:48px;background:url(mousie.gif)" id="mousie" tabindex="0"></div>
- How to track keys?
- We need to track arrow keys. So there are two events in our disposal:
keydownandkeyup. We choosekeydown, because it allows to cancel the default action, which is page scrolling. - How to move the mousie?
- Like any other element:
position:absolute,leftandtopchange depending on the key.
Changing position on click.
In the beginning, the mousie has static position. First, we need change it to absolute on click or on focus which gives better accessibility, because the focus can be given by keyboard tab too.
An absolutely positioned mousie will stick to the left-upper corner of the BODY. To keep mousie at same place, we need to set left/top to it’s current coordinates:
document.getElementById('mousie').onfocus = function() {
this.style.position = 'absolute'
var offset = getOffset(this)
this.style.left = offset.left + 'px'
this.style.top = offset.top + 'px'
}
The function
getOffset is described in Metrics.
Also, the pitfall is that if the mousie is surronded by other elements, they will shift. An absolutely positioned element jumps out of the flow:
<div style="color:green">Before</div> <div onclick="this.style.position = 'absolute'" style="cursor:pointer"> Click me </div> <div style="color:red">After</div>
To fix it, we need a placeholder or a wrapper, like this:
<div style="width:41px; height:48px"> <div style="width:41px;height:48px;background:url(mousie.gif)" id="mousie" tabindex="0"></div> </div>
The outer
DIV occupies the space no matter if the contents exists (in flow) or not.
Moving the beast
The codes for arrows are 37-38-39-40 (left-top-right-bottom).
document.getElementById('mousie').onkeydown = function(e) {
e = e || event
switch(e.keyCode) {
case 37: // left
this.style.left = parseInt(this.style.left)-this.offsetWidth+'px'
return false
case 38: // up
this.style.top = parseInt(this.style.top)-this.offsetHeight+'px'
return false
case 39: // right
this.style.left = parseInt(this.style.left)+this.offsetWidth+'px'
return false
case 40: // down
this.style.top = parseInt(this.style.top)+this.offsetHeight+'px'
return false
}
}
Note that the default action of arrows is to scroll the page. So we have to return false to prevent it.
There is no need to remove handlers on blur, because the browser will stop triggering keydown. So when a user blurs the mousie, it stops reacting on keys.
The final solution is tutorial/browser/events/mousie/index.html.
Create an input that warns user if the Caps Lock is on. Releasing Caps Lock removes the warning. This may help to prevent errors when entering password.
The source document: tutorial/browser/events/capslock-src/index.html.
How to track Caps Lock?
Unfortunately, there is no direct access to the state.
But we could use the events:
- Check
keypressevents. An uppercased char without shift or a lowercased char with shift means that Caps Lock is on. - Check
keydownfor Caps Lock key. It has keycode 20.
For reliability both keydown and keypress events should be tracked on page-level.
On page load, before anything was printed, we know nothing about Caps Lock, so the state is null:
var capsLockEnabled = null
When a key is pressed, we can try to check if character case and shift do not match:
document.onkeypress = function(e) {
e = e || event
var chr = getChar(e)
if (!chr) return // special key
if (chr.toLowerCase() == chr.toUpperCase()) {
// caseless symbol, like whitespace
// can't use it to detect Caps Lock
return
}
capsLockEnabled = (chr.toLowerCase() == chr && e.shiftKey) || (chr.toUpperCase() == chr && !e.shiftKey)
}
When a user presses Caps Lock, we should change current Caps Lock state. But we can do it only if we know it.
For example, when a user enteres page, we don’t know if Caps Lock is on. Then a keydown for Caps Lock detected. But we still don’t know what the new state is - did he disable Caps Lock or enable it.
document.onkeydown = function(e) {
e = e || event
if (e.keyCode == 20 && capsLockEnabled !== null) {
capsLockEnabled = !capsLockEnabled
}
}
Now, the input. The task is to show a warning about Caps Lock On to protect the user from password errors.
- First, the user focuses on it. We should show Caps Lock warning if we know it’s enabled.
- The user starts to type. Every
keypressbubbles up todocument.keypresshandler which updatescapsLockEnabled.We can’t use
input.onkeypressto indicate the state to the user, because it will work beforedocument.onkeypress(cause of bubbling) and hence before we know the Caps Lock state.There are many ways to solve this problem. We’ll stick to simplest and assign caps lock indication handler to
input.onkeyup. It always happens afterkeypress. - At last, user blurs the input. The Caps Lock warning may happen to be on, but it is not needed any more if the input is blurred. So we need to hide it.
The input checking code:
<input type="text" onkeyup="checkCapsWarning(event)" onfocus="checkCapsWarning(event)" onblur="removeCapsWarning()"/>
<div style="display:none;color:red" id="caps">Warning: Caps Lock is on!</div>
<script>
function checkCapsWarning() {
document.getElementById('caps').style.display = capsLockEnabled ? 'block' : 'none'
}
function removeCapsWarning() {
document.getElementById('caps').style.display = 'none'
}
</script>
The full code for the solution is here: tutorial/browser/events/capslock/index.html.
Create a DIV which becomes an editable TEXTAREA when Ctrl-E is pressed.
When in edit mode, the changes are saved to DIV on Ctrl-S and junked on Esc. After it, the TEXTAREA becomes the DIV again.
The contents is saved as HTML, tags should work.
Please look at how it should look: tutorial/browser/events/hotfield/index.html.
The source code is here: tutorial/browser/events/hotfield-src/index.html.
As you notice in the source code, #view is the DIV for the result and #area is the editable textarea.
The look
First, the look. Because we transform a DIV into TEXTAREA and back, we make them look almost same:
#view, #area {
height:150px;
width:400px;
font-family:arial;
}
The textarea should be emphased somehow. A possible way is to add aborder. But if I set the border, this will change the box, enlarge it and shift the text a little bit.
To make #area size same as #view we use padding:
#view {
/* padding + border = 3px */
padding: 2px;
border:1px solid black;
}
The #area
#area {
border: 3px groove blue;
padding: 0px;
display:none;
}
It is initially hidden. Also, the following piece hides extra border around focused textarea which appears in Chrome/Safari:
#area:focus {
outline: none; /* remove focus border in Safari */
}
Tracking codes
To track keys, we need their scan codes, not characters. That’s important, because such hotkey will work in all languages and all cases. So, keydown is a reasonable choice:
document.onkeydown = function(e) {
e = e || event
if (e.keyCode == 27) { // escape
cancel()
return false
}
if ((e.ctrlKey && e.keyCode == 'E'.charCodeAt(0)) && !area.offsetHeight) {
edit()
return false
}
if ((e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) && area.offsetHeight) {
save()
return false
}
}
In the example above, offsetHeight is checked to see if the element is visible. It is a very reliable way on all elements except for TR tag (works with tweaks).
Unlike simple display=='none' check it works with element hidden by styles (as we have here) and also for elements with hidden parents.
Editing
The following functions switch between modes. HTML is allowed, so the direct transform from/to TEXTAREA is possible.
function edit() {
view.style.display = 'none'
area.value = view.innerHTML
area.style.display = 'block'
area.focus()
}
function save() {
area.style.display = 'none'
view.innerHTML = area.value
view.style.display = 'block'
}
function cancel() {
area.style.display = 'none'
view.style.display = 'block'
}
The full solution is here: tutorial/browser/events/hotfield/index.html.
To test it, focus in the right iframe please.