Many of us are familiar with what CTRL + Z does (it undos the last thing you did in many applications) or what CTRL + O does (opens something).  These are called keyboard shortcuts.  Web applications can take advantage of keyboard shortcuts too with just a small amount of code.  This three part series will cover the following:

  1. How to Capture Keyboard Shortcuts with JavaScript
  2. Reacting to Keyboard Shortcut Events
  3. How To Use Keyboard Shortcuts For Good and not Evil

How to Capture Keyboard Shortcuts with JavaScript | Demo: Click Here

Obviously, we need to be able to capture when a user presses a key on the keyboard. We also need to figure out which key was pressed and then do something about it.  To do this we need to take advantage of two different events in JavaScript: onkeydown and onkeyup.  You might be wondering why we wouldn't use JavaScript's onkeypress event here? In JavaScript, each key that is pressed fires off three events: one when the key is fully pressed (onkeydown), one when the key is released (onkeyup), and one when the key is both fully pressed and then released (onkeypress).  If we only used the onkeypress event, we couldn't compare previous key presses to the current key pressed; which basically means our application couldn't take advantage of modifiers and other combinations (i.e. we could act on either the "CTRL" or "Z" keys being pressed, but not both when used together one right after the other).  

To capture when the user presses a key on the page we must tell the browser that we want to record both events no matter what element on the page has focus:

 

We are attaching the events to the HTML body tag.  When the user fully presses a key on their keyboard, the keyman.add(event) function is called no matter what element on the page has focus.  Then when the user releases the key, the keyman.remove(event) function is called.  Both functions take a window.event parameter to help us determine which key was pressed.  "keyman" is a JavaScript object literal that is defined below:

var keyman = {
    code_registry: [],
    key_registry: [],

    add: function(e) {

    },
    
    remove: function(e) {

    }
};

The keyman.add(event) function

In this function, we want to figure out what key is currently being pressed by the user and store both the key's code and its value in an array.  To do this, we're going to add two functions to the keyman object:

getCode: function(e) { 
    e = window.event || e; 
    var code = e.keyCode || e.which; 
    return code; 
}, 

getKey: function(e) { 
    e = window.event || e; 
    var code = e.keyCode || e.which; 
    return String.fromCharCode(code); 
}

The getCode() function returns the actual character code from the event.  The getKey() function returns the actual key's value from the event.  For example, if a user pressed the "a" key, the getCode function would return 65 and the getKey function would return "a".  We need both the code and value of the key because some keys may not have a descriptive value that we can use (like the Escape key).

Next, let's add some code to the keyman.add(event) function to store the key code and value when the user fully presses a key:

add: function(e) {
    var code = this.getCode(e);//key code (i.e. if "a" is pressed, then this function returns "65")
    var key = this.getKey(e).toUpperCase(); //key value (i.e. if "a" is pressed, then this function returns "A")

    //if the key code and value aren't already in the arrays, then add them!
    if(!this.code_registry.inArray(code) && !this.key_registry.inArray(key)) {
        this.code_registry.push(code);
        this.key_registry.push(key);
    }

    return this;
};

The code above simply adds the key code and value of the button being pressed to the code_registry and key_registry arrays respectively.  Notice the inArray function that is called twice in the above code.  Not all browsers implement this function so you'll need to implement it yourself, here's the code:

Array.prototype.inArray = function (value) {
    var i;
    for (i=0; i < this.length; i++) {
        if (this[i] === value) {
            return true;
        }
    }

    return false;
};

The inArray() function returns true if the value is found within the array, false otherwise.  Let's move on to the keyman.remove(event) function now.

The keyman.remove(event) function

In this function, we simply want to remove the key code and value of the key that was just released from the code_registry and key_registry arrays respectively.  You might be asking yourself, why would we need to store the key's code and value and then remove them so quickly once the key is released? The purpose is to capture combinations of key presses.  For example, when I press the "CTRL" key, the keyman.add(event) function is immediately called and stores the key's code and value. While I am holding down the "CTRL" key, I may press the "Z" key and again the keyman.add(event) function is called and stores the key's code and value in the registries.  Now that the keyman object "knows" that the "CTRL" and "Z" keys have both been pressed we can act on it and make something happen (like undoing whatever was just done). Making things happen once a certain combination of keys are pressed is what the next article in this series will cover.  But for now, here's the code for the keyman.remove(event) function:

remove: function(e) {
    var code = this.getCode(e);
    var key = this.getKey(e).toUpperCase();
    
    var index1 = this.code_registry.indexOf(code);
    this.code_registry.splice(index1, 1);
    
    var index2 = this.key_registry.indexOf(key);
    this.key_registry.splice(index2, 1);
    
    return this;
}

Notice the indexOf() function that is called twice in the above code.  Like the inArray() function, this function is also not implemented in all browsers so you will need to implement it yourself.  I'm a nice guy though so I've provided the common code below:

if (!Array.prototype.indexOf)
{
  Array.prototype.indexOf = function(elt)
  {
    var len = this.length;

    for (from = 0; from < len; from++)
    {
    	if (from in this && this[from] === elt) {
    		return from;
        }
    }
    return -1;
  };
};

This function returns the index of the element in the array or -1 if the element is not found.  

The Keyman Object

So far, we've told the browser we want to record when the user fully presses a key and when the user releases a key.  The complete code for this looks like the following:

var keyman = {
    code_registry: [],
    key_registry: [],

    add: function(e) {
        var code = this.getCode(e);//key code (i.e. if "a" is pressed, then this function returns "65")
        var key = this.getKey(e).toUpperCase(); //key value (i.e. if "a" is pressed, then this function returns "A")

        //if the key code and value aren't already in the arrays, then add them!
        if(!this.code_registry.inArray(code) && !this.key_registry.inArray(key)) {
            this.code_registry.push(code);
            this.key_registry.push(key);
        }

        return this;
    },

    getCode: function(e) { 
        e = window.event || e; 
        var code = e.keyCode || e.which; 
        return code; 
    }, 

    getKey: function(e) { 
        e = window.event || e; 
        var code = e.keyCode || e.which; 
        return String.fromCharCode(code); 
    },

    remove: function(e) {
        var code = this.getCode(e);
        var key = this.getKey(e).toUpperCase();
    
        var index1 = this.code_registry.indexOf(code);
        this.code_registry.splice(index1, 1);
    
        var index2 = this.key_registry.indexOf(key);
        this.key_registry.splice(index2, 1);
    
        return this;
    }
};

Check out the Demonstration of How to Capture Keyboard Shortcuts with JavaScript.

Part 2 - Reacting to Keyboard Shortcut Events

Part 2 of this series will go over how to create definitions for keyboard shortcuts so that when one of the definitions is called, we can act on it. The next article contains all the magic, just click on Reacting to Keyboard Shortcut Events.

Comments? Questions?

If you have any comments or questions, please leave a comment below, thanks for reading!