This is the second part of a three part series on how to implement keyboard shortcuts in a web application. In this part, we'll go over how to create definitions for keyboard shortcuts so that when one of the definitions is called (i.e. when a user presses CTRL + Z) then we can act on it.  If you haven't read the first part of this series yet, just click below:

  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

Our JavaScript Object Literal Currently Looks Like This

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

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

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

        //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;
    }
};

You can see a demo of the class as it is above here: Demonstration of How To Capture Keyboard Shortcuts.

Reacting To Keyboard Shortcut Events | Demo: Click Here

Currently, we're capturing and logging when a user presses a key on the keyboard, but there aren't any events that happen, or called, after a user presses the correct combination of keys.  We need a way to tell our application what combination of keys we are looking for and what should happen once the user presses the right keys (in order).  Let's begin by adding a register() function that helps us define our keyboard shortcut:

registry: [], //an array that holds all of our registry entries

/**
* register - registers a combination of key presses that produce a keyboard shortcut
*    @param id - a unique identifier that describes the shortcut
*    @param key - a set of keys (i.e. a, b, c, etc); for multiple combos 
*          separate keys with the + sign (i.e. j+k+l); can optionally use key code too (i.e. 65 for 'a', etc)
*    @param keydown - a function that executes when this shortcut has all the keys pressed down
*    @param mods - a comma separated list of modifiers (CTRL, SHIFT, and/or ALT | i.e. CTRL,ALT)
*/
register: function(id, key, keydown, mods) {
    mods = (mods) ? mods.split(',') : null;
    this.registry.push([id, key.toUpperCase(), keydown, mods]);
    return this;
}

This method takes 4 parameters with the first two parameters being the most important.  The "id" parameter is necessary in case we need to remove (unregister) the shortcut for any reason.  The "key" parameter is obviously needed so that we can compare the keys the user presses to the ones we define - once we make a match, the events can be called. Note here that you can optionally use a key code (instead of key value) for the "key" parameter.

Here's a couple examples of how to call the register function (this code can be placed anywhere in the document as long as the keyman object is defined above (or before) the code is executed):

//SHIFT + S
keyman.register('SaveSomething', 's', function(event) {
    //do something when all keys are pressed down
    alert('all keys are pressed down!');
}, 'SHIFT');

//Escape key (no modifiers here)
keyman.register('CloseSomething', '27', function(event) {
    alert("I'm closing something here with the Escape key! Maybe a popup window! Who knows!");
});

In the first example, as soon as I press and hold down the SHIFT key and then the "S" key, an alert message displays saying "all keys are pressed down!".

In the second example, as soon as I press the escape key, I get an alert that says "I'm closing something here with the Escape key! Maybe a popup window! Who knows!".  Of course, you don't have to make your functions say this, that would be lame, instead make them do something useful! 

The problem with both examples is that there is nothing in our keyman class that calls the keydown function in our definitions once the right set of keys are pressed! To do this, we need to modify the add function a bit and add a new function "press" that will check if the user has pressed the right combination and run any events.

The Modified keyman.add(event) Function

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

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

    //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);
    }

    this.press(e);

    return this;
},

When I said we'd be modifying this a little bit, I really meant it! All we added was a call to the press(event) that we'll be calling shortly. It's important to know that once a user presses a key on their keyboard, the add function runs and the press function is called right after.

The keyman.press(event) Function

This function is where the real work comes in.  It cycles through our definitions and determines if all the keys in the definition have been pressed by checking the key and code registries (arrays). If all the keys have been pressed (including the right modifiers, if any were specified), then the keydown function is called.

press: function(e) {			
    //check if any modifiers are currently pressed
    var mods = [];
	
    if(e.shiftKey == 1) mods.push('SHIFT');
    if(e.ctrlKey == 1) mods.push('CTRL');
    if(e.altKey == 1) mods.push('ALT');

    //loop through registry to find a match for the current key presses
    for(i = 0; i < this.registry.length; i++) {
	var r_id = this.registry[i][0];
	var r_key = this.registry[i][1];
	var r_keydown = this.registry[i][2];
	var r_mods = this.registry[i][3];
	
	var all_pressed = true;
	var keys = []; //keys in this definition, helpful for removal later
	
	if(r_key.indexOf('+') != -1) {
	    keys = r_key.split('+');
		
	    for(j = 0; j < keys.length; j++) {
	    	if(!this.key_registry.inArray(keys[j]) && !this.code_registry.inArray(parseInt(keys[j]))) {
	            all_pressed = false;
	    	}
	    }
	} else if(!this.key_registry.inArray(r_key) && !this.code_registry.inArray(parseInt(r_key))) {
	    all_pressed = false;
	} else {
	    keys.push(r_key);
	}

	if(all_pressed) {
	    //check modifiers if they were defined and are pressed
	    var mods_pressed = true;

	    if(r_mods && r_mods.length > 0) {
		for(j = 0; j < r_mods.length; j++) {
	            var mod = r_mods[j];

	            if(!mods.inArray(mod)) {
			mods_pressed = false;
		    }
		}
	    }

	    if(mods_pressed) {
                if(typeof r_keydown == 'function') {
		    r_keydown(e);
	        }

		//remove all the pressed keys from the registry
	        for(j = 0; j < keys.length; j++) {
		    var index = this.key_registry.indexOf(keys[j]);
		    this.key_registry.splice(index, 1); //remove key from registry
		    this.code_registry.splice(index, 1); //remove code from registry
		}
	    }
	}
    }

    return this;
}

This is a pretty heavy function, and may be a little confusing so let's go through it:

1. The first thing we do is check if any modifiers are currently being pressed by using the passed in window.event object.

2. Next, we loop through the registry and compare our shortcut definitions to the keys that are currently being pressed down.

3. Inside the for loop you will see that if the key parameter (r_key) contains a + sign, we create an array and check each of the keys to see if they are contained inside either the key or code registries.  If they aren't, the "all_pressed" boolean variable becomes false.  If there wasn't a + sign in the key parameter (r_key), we simply check to see if r_key is in either the key or code registries; again, if not, the "all_pressed" boolean variable becomes false. 

4. If all the keys have been pressed, we check to see if all the modifiers (contained in the definition) have been pressed as well.  If everything matches up, we can then call the keydown (r_keydown) function to run some code that we want to happen when a user presses the right combination of keys in our definition.

Here's Our Updated Keyman Object

var keyman = {
    code_registry: [],
    key_registry: [],
    registry: [], //an array that holds all of our registry entries

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

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

        //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);
        }

        this.press(e);

        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); 
    },

    press: function(e) {
        //check if any modifiers are currently pressed
        var mods = [];
        
        if(e.shiftKey == 1) mods.push('SHIFT');
        if(e.ctrlKey == 1) mods.push('CTRL');
        if(e.altKey == 1) mods.push('ALT');

        //loop through registry to find a match for the current key presses
        for(i = 0; i < this.registry.length; i++) {
            var r_id = this.registry[i][0];
            var r_key = this.registry[i][1];
            var r_keydown = this.registry[i][2];
            var r_mods = this.registry[i][3];

            var all_pressed = true;
            var keys = []; //keys in this definition, helpful for removal later
    
            if(r_key.indexOf('+') != -1) {
                keys = r_key.split('+');
            
                for(j = 0; j < keys.length; j++) {
                    if(!this.key_registry.inArray(keys[j]) && !this.code_registry.inArray(parseInt(keys[j]))) {
                       all_pressed = false;
                    }
                }
            } else if(!this.key_registry.inArray(r_key) && !this.code_registry.inArray(parseInt(r_key))) {
                all_pressed = false;
            } else {
                keys.push(r_key);
            }

            if(all_pressed) {
                //check modifiers if they were defined and are pressed
                var mods_pressed = true;

                if(r_mods && r_mods.length > 0) {
                    for(j = 0; j < r_mods.length; j++) {
                        var mod = r_mods[j];

                        if(!mods.inArray(mod)) {
                            mods_pressed = false;
                        }
                    }
                }

                if(mods_pressed) {
                    if(typeof r_keydown == 'function') {
                        r_keydown(e);
                    }

                    //remove all the pressed keys from the registry
                    for(j = 0; j < keys.length; j++) {
                        var index = this.key_registry.indexOf(keys[j]);
                        this.key_registry.splice(index, 1); //remove key from registry
                        this.code_registry.splice(index, 1); //remove code from registry
                    }
                }
            }
        }

        return this;
    },

    /**
    * register - registers a combination of key presses that produce a keyboard shortcut
    *    @param id - a unique identifier that describes the shortcut
    *    @param key - a set of keys (i.e. a, b, c, etc); for multiple combos 
    *          separate keys with the + sign (i.e. j+k+l); can optionally use key code too 
    *          (i.e.  65 for 'a', etc)
    *    @param keydown - a function that executes when this shortcut has all the keys 
    *          pressed down
    *    @param mods - a comma separated list of modifiers (CTRL, SHIFT, and/or ALT | 
    *          i.e. CTRL,ALT)
    */
    register: function(id, key, keydown, mods) {
        mods = (mods) ? mods.split(',') : null;
        this.registry.push([id, key.toUpperCase(), keydown, mods]);
        return this;
    },

    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;
    }
};

To view a demonstration of this class click on Reacting To Keyboard Shortcut Events.

Part 3 Coming Soon

We've still got some more work to do!  We need to be able to prevent keyboard shortcuts from activating if a user is typing something into a textfield on a form.  So without further ado, let's move on to How To Use Keyboard Shortcuts for Good and not Evil!

Comments? Questions?

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