Author: Mihai Şucan (ROBO Design)
In the first part of the article we have added keyboard shortcuts to a painting application. In the second part we implemented a way to move the pointer inside the drawing canvas using keyboard shortcuts. Now we will look into the cross-browser compatibility layer implementation for handling all the differences between browsers.
To make it easier to follow along with the code walkthrough presented below, download the full code example and follow along with it as you read the article.
There are important differences between browsers with regards to keyboard events. Differences apply to the properties and the values they provide in the DOM Event objects. There are also differences that affect the firing of the events, and default action prevention. Last but not least, the same browser can behave differently on a different operating system.
Keyboard events have several important properties:
keyCode
gives the code of the key for a specific event.
For example, the key code for Enter is 13, or 17 for
Control. Some keys, mostly the alpha-numeric ones, have the
same key code as the character Unicode decimal value. For example, the key
P has the key code 80, which is the same as the Unicode decimal
for the "P" capital letter.charCode
is only provided by Firefox (Gecko), Safari
(Webkit) and Google Chrome (Webkit) when the key generates a character.
It holds the Unicode decimal for that character.which
is arbitrarily available in most browsers, except
Internet Explorer.keyIdentifier
is the new property defined by the DOM 3 Events
specification. This property is only available for the
keydown
and keyup
events, in Webkit and
Konqueror 4. The values it holds are either key names like
Enter, PageDown etc., or the Unicode character
generated by the key event, using the literal notation "U+XXXX". If you
press the P key, then the keyIdentifier
value is
"U+0050".So this is the big picture - you are already getting an idea of how difficult is to identify a key from a keyboard event. In short, the browser differences you need to be aware of are as follows:
keyCode
property holds the code of the key only
during the keydown
and keyup
events in most
browsers. In the keypress
event, this property actually
holds the character Unicode decimal value. For example, if you press the
key P without holding the Shift key down, then
keyCode
is 80 in the keydown
and the
keyup
events. In the keypress
event the property
value is 112 - the Unicode decimal for the "p" character.charCode
property, in the
keypress
event you cannot easily determine the difference
between a key code and a character code. Therefore, you need to remember
the key code from the keydown
event.keyIdentifier
property. For some keys, the value is "U+0000"
instead of "Unidentified" as the specification says it should be when the
key is not recognized.keyIdentifier
property. Instead, it gives you the character
directly, as-is.As I said before, there are differences between browsers with regards to event firing as well:
keypress
event for all keys, except for modifiers. Konqueror (KHTML) also fires
this event for modifiers.keypress
event
only for characters (alpha-numeric and symbols). Additionally, the event
is fired for three keys: Escape, Space and
Enter. Safari and Google Chrome (both Webkit) do the same,
with the exception that they do not fire the event for
Escape.Another important difference between browsers is how they repeat the
keyboard events, while the user holds down the same key for longer periods
of time. For example, Firefox on Linux only repeats the
keypress
event. However, Firefox on Windows repeats both
events, the keydown
and the keypress
events.
This is only an overview of browser differences, and it's not highly detailed. For more details, you might want to check out the code comments in the lib.js file.
The minimal JavaScript library code tries to determine the accurate
keyCode
and charCode
. The keypress
event is dispatched synthetically for all keys, except modifiers. The
keydown
event is never repeated; only the keypress
event is repeated.
Here is the basic structure of the KeyboardEventListener
class:
lib.dom.KeyboardEventListener = function (elem_, handlers_) {
var keyCode_ = null;
var key_ = null;
var charCode_ = null;
var char_ = null;
var repeat_ = false;
this.attach = function () { ... };
this.detach = function () { ... };
function dispatch (type, ev) { ... };
function keydown (ev) { ... };
function keypress (ev) { ... };
function keyup (ev) { ... };
function isModifierKey (key) { ... };
function firesKeyPress (ev) { ... };
function findKeyCode (ev) { ... };
function findCharCode (ev) { ... };
this.attach();
};
When you create a new instance of the KeyboardEventListener
class you need to provide the DOM Element you want to listen for keyboard
events on, the elem_
argument. You also have to provide the
event handler(s) you want for each keyboard event: keydown
,
keypress
and keyup
. The handlers_
argument must be an object holding the event names as properties, with the
values are the functions, the handlers you want for each event. Example
code:
var klogger = function (ev) {
console.log(ev.type +
' keyCode_ ' + ev.keyCode_ +
' key_ ' + ev.key_ +
' charCode_ ' + ev.charCode_ +
' char_ ' + ev.char_ +
' repeat_ ' + ev.repeat_);
};
var kbListener = new lib.dom.KeyboardEventListener(window,
{keydown: klogger,
keypress: klogger,
keyup: klogger});
// later when you are done...
kbListener.detach();
The event listeners are automatically attached to the element you give
when the KeyboardEventListener
class is instanced. Here is the
code used for attaching and detaching the event listeners:
/**
* Attach the keyboard event listeners to the current DOM element.
*/
this.attach = function () {
keyCode_ = null;
key_ = null;
charCode_ = null;
char_ = null;
repeat_ = false;
elem_.addEventListener('keydown', keydown, false);
elem_.addEventListener('keypress', keypress, false);
elem_.addEventListener('keyup', keyup, false);
};
/**
* Detach the keyboard event listeners from the current DOM element.
*/
this.detach = function () {
elem_.removeEventListener('keydown', keydown, false);
elem_.removeEventListener('keypress', keypress, false);
elem_.removeEventListener('keyup', keyup, false);
keyCode_ = null;
key_ = null;
charCode_ = null;
char_ = null;
repeat_ = false;
};
As you can see, the event handlers are not used here. The event handlers
are the private functions, keydown()
, keypress()
and keyup()
. Each of these functions will deal individually
with browser differences, and they will call the event handlers you
provided.
Here we have the code for the keydown()
event handler:
function keydown (ev) {
var prevKey = key_;
charCode_ = null;
char_ = null;
findKeyCode(ev);
ev.keyCode_ = keyCode_;
ev.key_ = key_;
ev.repeat_ = key_ && prevKey == key_ ? true : false;
repeat_ = ev.repeat_;
// Do not repeat keydown events.
if (!repeat_) {
dispatch('keydown', ev);
}
// MSIE and Webkit only fire the keypress event for characters
// (alpha-numeric and symbols).
if (!isModifierKey(key_) && !firesKeyPress(ev)) {
ev.type_ = 'keydown';
keypress(ev);
}
};
This function determines the keyCode_
and the
key_
variables for the current event, using the
findKeyCode()
function. The two variables are added as new
properties to the DOM Event object, such that the event handler you provided
will be able to use the values. If the keydown
event is
repeated, then ev.repeat_
will be set to true. The event is
dispatched to the keydown
handler you provided using the
dispatch()
function only if the event is not repeating.
Another issue being tackled by the keydown()
function is the
firing of the keypress
event. As described above, Webkit and IE
do not always fire the event. Therefore, if the key is not a modifier, and
if the firesKeyPress()
function returns false, then we manually
invoke the keypress()
event handler.
The charCode_
and char_
variables are reset to
null, because the character is never determined during the
keydown
event.
Before we see the code for the functions used by the keydown
event handler, let's take a look at the code of the other two event
handlers. Here is the keypress()
function:
function keypress (ev) {
if (!keyCode_) {
findKeyCode(ev);
repeat_ = false;
}
ev.keyCode_ = keyCode_;
ev.key_ = key_;
findCharCode(ev);
ev.charCode_ = charCode_;
ev.char_ = char_;
// Any subsequent keypress event is considered a repeated keypress (the user
// is holding down the key).
ev.repeat_ = repeat_;
if (!repeat_) {
repeat_ = true;
}
if (!isModifierKey(key_)) {
dispatch('keypress', ev);
}
};
This code expects the keyCode_
and key_
variables to be set from the keydown
event; we can only
determine the charCode_
and the char_
variables
using the findCharCode()
function. As described above, in
general cases the keydown
event gives the key code, and the
keypress
event gives the character code. Still, if for some
reason the key was not determined during the keydown
event, the
code tries again.
The keypress
event is only dispatched to the event handler
you provided if it is not a modifier key. Here is the keyup
event handler:
function keyup (ev) {
findKeyCode(ev);
ev.keyCode_ = keyCode_;
ev.key_ = key_;
// Provide the character info from the keypress event in keyup as well.
ev.charCode_ = charCode_;
ev.char_ = char_;
dispatch('keyup', ev);
keyCode_ = null;
key_ = null;
charCode_ = null;
char_ = null;
repeat_ = false;
};
Here we are trying to determine the keyCode_
for the
keyup
event again, even if we might already have it from the
previous keydown
event. This is needed because the user might
press some key which only generates the keydown
and the
keypress
events, after which a sudden keyup
event
is fired for a completely different key. For example, in Opera press
F2 then Escape. It will first generate two events
- keydown
and keypress
- for the F2
key. When you press Escape to close the dialog box, the script
receives the keyup
event for the Escape key.
After the keyup
event is dispatched to the event handler you
provided, any information stored during the event flow is lost, to prevent
any mix-up.
Here is the simple isModifierKey()
function:
function isModifierKey (key) {
switch (key) {
case 'Shift':
case 'Control':
case 'Alt':
case 'Meta':
case 'Win':
return true;
default:
return false;
}
};
The function is as straightforward as it can be.
Next, let's look at the function that determines if the browser will fire
a keypress
event or not:
function firesKeyPress (ev) {
if (!lib.browser.msie && !lib.browser.webkit) {
return true;
}
// Check if the key is a character key, or not.
// If it's not a character, then keypress will not fire.
// Known exceptions: keypress fires for Space, Enter and Escape in MSIE.
if (key_ && key_ != 'Space' && key_ != 'Enter' && key_ != 'Escape' &&
key_.length != 1) {
return false;
}
// Webkit doesn't fire keypress for Escape as well ...
if (lib.browser.webkit && key_ == 'Escape') {
return false;
}
// MSIE does not fire keypress if you hold Control / Alt down, while Shift is off.
if (lib.browser.msie && !ev.shiftKey && (ev.ctrlKey || ev.altKey)) {
return false;
}
return true;
};
There is no way to detect if the keypress
event will really
fire or not. We simply rely on the knowledge we have based on testing and
online documentation.
The keyCode_
and key_
variables above are
determined by the findKeyCode()
function:
function findKeyCode (ev) {
/*
* If the event has no keyCode/which/keyIdentifier values, then simply do
* not overwrite any existing keyCode_/key_.
*/
if (ev.type == 'keyup' && !ev.keyCode && !ev.which && (!ev.keyIdentifier ||
ev.keyIdentifier == 'Unidentified' || ev.keyIdentifier == 'U+0000')) {
return;
}
keyCode_ = null;
key_ = null;
// Try to use keyCode/which.
if (ev.keyCode || ev.which) {
keyCode_ = ev.keyCode || ev.which;
// Fix Webkit quirks
if (lib.browser.webkit) {
// Old Webkit gives keyCode 25 when Shift+Tab is used.
if (keyCode_ == 25 && this.shiftKey) {
keyCode_ = lib.dom.keyNames.Tab;
} else if (keyCode_ >= 63232 && keyCode_ in lib.dom.keyCodes_Safari2) {
// Old Webkit gives wrong values for several keys.
keyCode_ = lib.dom.keyCodes_Safari2[keyCode_];
}
}
// Fix keyCode quirks in all browsers.
if (keyCode_ in lib.dom.keyCodes_fixes) {
keyCode_ = lib.dom.keyCodes_fixes[keyCode_];
}
key_ = lib.dom.keyCodes[keyCode_] || String.fromCharCode(keyCode_);
return;
}
// Try to use ev.keyIdentifier. This is only available in Webkit and
// Konqueror 4, each having some quirks. Sometimes the property is needed,
// because keyCode/which are not always available.
var key = null,
keyCode = null,
id = ev.keyIdentifier;
if (!id || id == 'Unidentified' || id == 'U+0000') {
return;
}
if (id.substr(0, 2) == 'U+') {
// Webkit gives character codes using the 'U+XXXX' notation, as per spec.
keyCode = parseInt(id.substr(2), 16);
} else if (id.length == 1) {
// Konqueror 4 implements keyIdentifier, and they provide the Unicode
// character directly, instead of using the 'U+XXXX' notation.
keyCode = id.charCodeAt(0);
key = id;
} else {
/*
* Common keyIdentifiers like 'PageDown' are used as they are.
* We determine the common keyCode used by Web browsers, from the
* lib.dom.keyNames object.
*/
keyCode_ = lib.dom.keyNames[id] || null;
key_ = id;
return;
}
// Some keyIdentifiers like 'U+007F' (127: Delete) need to become key names.
if (keyCode in lib.dom.keyCodes && (keyCode <= 32 || keyCode == 127 || keyCode
== 144)) {
key_ = lib.dom.keyCodes[keyCode];
} else {
if (!key) {
key = String.fromCharCode(keyCode);
}
// Konqueror gives lower-case chars
key_ = key.toUpperCase();
if (key != key_) {
keyCode = key_.charCodeAt(0);
}
}
// Correct the keyCode, make sure it's a common keyCode, not the Unicode
// decimal representation of the character.
if (key_ == 'Delete' || key_.length == 1 && key_ in lib.dom.keyNames) {
keyCode = lib.dom.keyNames[key_];
}
keyCode_ = keyCode;
};
The findKeyCode()
function tries to determine the key and
its code using ev.keyCode
or ev.which
. If these
two properties are not available, then the ev.keyIdentifier
property is checked.
When the keyCode
or which
property is
available, the value is adjusted/corrected for each browser. Mainly, Webkit
is known to give the key code 25 when the key combination Shift
+ Tab is used. Additionally, older Webkit versions gave incorrect
values for several keys. There are several incorrect key codes in several
browsers, so we use the lib.dom.keyCodes_fixes
map.
We have the lib.dom.keyCodes
object holding the list of key
codes associated to key names/identifiers. This is needed so we can provide
an easy to remember, human-readable value for the key_
property, which is added to the DOM Event object. If the key code is not
found in the list, the code is used as a Unicode decimal value, converted to
a string with the String.fromCharCode()
function.
If the keyIdentifier
event property is used, we will try to
parse the "U+XXXX" notation. We also check to see if it is a single
character, like in Konqueror 4. If the value is not in one of these forms,
it is considered to hold the key name, like Enter or
PageDown. We have the lib.dom.keyNames
object
holding a list of common key names associated to their key code. The
key_
value will be the same as the one in the
keyIdentifier
property.
The DOM 3 Events specification defines that for some keys, like
Delete; the value for the keyIdentifier
property is
that of the Unicode character. So, from the Unicode specification, or from
the ASCII table, "U+007F" is Delete, "U+0009" is Tab,
etc. In these cases, the findKeyCode()
tries to also determine
the key name.
The function that determines the charCode_
and the
char_
properties is as follows:
function findCharCode (ev) {
charCode_ = null;
char_ = null;
// Webkit and Gecko implement ev.charCode.
if (ev.charCode) {
charCode_ = ev.charCode;
char_ = String.fromCharCode(ev.charCode);
return;
}
if (ev.keyCode || ev.which) {
var keyCode = ev.keyCode || ev.which;
var force = false;
// We accept some keyCodes.
switch (keyCode) {
case lib.dom.keyNames.Tab:
case lib.dom.keyNames.Enter:
case lib.dom.keyNames.Space:
force = true;
}
// Do not consider the keyCode a character code, if during the keydown
// event it was determined the key does not generate a character, unless
// it's Tab, Enter or Space.
if (!force && key_ && key_.length != 1) {
return;
}
// If the keypress event at hand is synthetically dispatched by keydown,
// then special treatment is needed. This happens only in Webkit and MSIE.
if (ev.type_ == 'keydown') {
var key = lib.dom.keyCodes[keyCode];
// Check if the keyCode points to a single character.
// If it does, use it.
if (key && key.length == 1) {
charCode_ = key.charCodeAt(0); // keyCodes != charCodes
char_ = key;
}
} else if (keyCode >= 32 || force) {
// For normal keypress events, we are done.
charCode_ = keyCode;
char_ = String.fromCharCode(keyCode);
}
if (charCode_) {
return;
}
}
/*
* Webkit and Konqueror do not provide a keyIdentifier in the keypress
* event, as per spec. However, in the unlikely case when the keyCode is
* missing, and the keyIdentifier is available, we use it.
*
* This property might be used when a synthetic keypress event is generated
* by the keydown event, and keyCode/charCode/which are all not available.
*/
var c = null,
charCode = null,
id = ev.keyIdentifier;
if (id && id != 'Unidentified' && id != 'U+0000' &&
(id.substr(0, 2) == 'U+' || id.length == 1)) {
// Characters in Konqueror...
if (id.length == 1) {
charCode = id.charCodeAt(0);
c = id;
} else {
// Webkit uses the 'U+XXXX' notation as per spec.
charCode = parseInt(id.substr(2), 16);
}
if (charCode == lib.dom.keyNames.Tab ||
charCode == lib.dom.keyNames.Enter ||
charCode >= 32 && charCode != 127 &&
charCode != lib.dom.keyNames.NumLock) {
charCode_ = charCode;
char_ = c || String.fromCharCode(charCode);
return;
}
}
// Try to use the key determined from the previous keydown event, if it
// holds a character.
if (key_ && key_.length == 1) {
charCode_ = key_.charCodeAt(0);
char_ = key_;
}
};
The findCharCode()
first tries to use the
charCode
event property. If it is available, then we are
done.
When the event keyCode
or which
property needs
to be used, we must check if the key determined during the
keydown
event generates a character or not. If it is
PageDown or some other non-character key, then we cannot use the
keyCode
property of the current keypress
event.
If the keyCode
and the which
event properties
are not available, we try the keyIdentifier
property. This
property is not available in the keypress
event, but it can be
available when the keydown
event synthetically dispatches the
keypress
event. The keyIdentifier
property is
checked for a Unicode character value of the form "U+XXXX". For Konqueror
compatibility, we also check if the property value is only one character.
These values are used only if they do not hold something like "U+007F" (the
Delete key).
The event dispatcher function code:
function dispatch (type, ev) {
if (!handlers_[type]) {
return;
}
var handler = handlers_[type];
if (type == ev.type) {
handler.call(elem_, ev);
} else {
// This happens when the keydown event tries to dispatch a keypress event.
var ev_new = {};
lib.extend(ev_new, ev);
ev_new.type = type;
// Make sure preventDefault() still works
ev_new.preventDefault = function () {
ev.preventDefault();
};
handler.call(elem_, ev_new);
}
};
This function checks if you provided an event handler for the given event
type
. If you have one, then the handler is invoked with the
updated DOM Event object. The DOM Event object is faked if the event type
being dispatched does not match the type of the original event - this
happens when the keydown
event handler dispatches
a keypress
event.
That is all! You should take a look into the new lib.js file and check out how everything ties together.
This three-part article series has provided pretty much everything you need to know to implement cross platform keyboard controls for complicated web applications. It's now up to you to start taking these techniques, experimenting with them, and making use of them in your own applications.
In this article series we tackled one aspect from what it takes to make a mature web application; there are always other things to do, more improvements to make! Some more improvements you may want to think about when you get time are:
If you want to learn more, stick around at DevOpera and also check the upcoming changes to PaintWeb - the open-source web application this article is based on.