This is the last part of a three part series on how to implement keyboard shortcuts in a web application. In this part, we'll update our keyman object and prevent any keyboard shortcuts from activating if a user is typing into a textfield on a form. We'll also go over a couple problems you might run into when using keyboard shortcuts. If you missed the first or second part of this series, you can click either of the below links to get to them and get caught up!

  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

This Is What Our Keyman Object Looks Like So Far

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 see a demonstration of how it works so far, check out Demonstration on Reacting to Keyboard Shortcuts.

As you can see in the demonstration (right-click and view the source), three keyboard shortcuts were registered with each one reacting in a different way when the user presses the right combination of keys. The problem with our implementation thus far is it doesn't prevent events from running when a user is typing into a textfield on a form. To do this, we need to modify the keyman.press(event) function to catch when the target of the passed in window.event object is a field on a form.

Modifying the keyman.press(event) Function

getTarget: function(e) {
    e = window.event || e;
    var target = e.target || e.srcElement;
    return target;
},

press: function(e) {
    var target = this.getTarget(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');

    var tags = ['INPUT','SELECT','TEXTAREA'];

    if(tags.inArray(target.tagName)) {
        return this;   
    }

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

First off, we added the getTarget(event) function. This function retrieves the element that serves as the target of the event that's passed in. So when a user is typing into a textfield, for example, the target element will be the textfield. Next, the press(event) function was modified to check the target element's tag name and match it against INPUT, SELECT, and TEXTAREA. If the tag name is any of those three then we don't want to run any keyboard shortcut events; so we return from the function.

Handling Duplicate Registrations

So what would happen if we registered two keyboard shortcuts (lets say, SHIFT + S) that do two different things? Our Keyman object would simply execute both shortcut events! We likely don't want that to happen and the best we can do is check to see if the keys we want to register have already been registered. Another option is to first unregister our set of keys. Let's provide both options:

indexOf: function(id_or_key, modifiers) {
    var index = -1;
    
    for(i = 0; i < this.registry.length; i++) {
        if(this.registry[i][0] == id_or_key || this.registry[i][1] == id_or_key) {
            if(modifiers) {
                if(this.registry[i][3].join(',') == modifiers) {
                    index = i;
                    break;
                }
            } else {
                index = i;
                break;
            }
        }
    }

    return index;
},

unregister: function(id_or_key, modifiers) {
    var index = this.indexOf(id_or_key, modifiers);

    if(index > 0) {
        this.registry.splice(index, 1);
        return true;
    } else {
        return false;
    }
}

The indexOf() function looks through each definition in the registry and returns the index value if the definition was found, or -1 if it could not find the definition. The unregister() function uses the indexOf() function to unregister the keyboard shortcut definition if found (returns true), otherwise it returns false. With these two functions, a duplicate registration can now be prevented. Here's a couple examples of how you could use both.

if(keyman.indexOf('SaveSomething') == -1) {
    keyman.register('SaveSomething','s',function(event) {
        alert("We're saving something!!");
    }, 'SHIFT');
}

keyman.unregister('S', 'SHIFT');
keyman.register('SaveSomething','s',function(event) {
    alert("We're saving something again!!");
}, 'SHIFT');

In the first example, we only register the 'SaveSomething' keyboard shortcut if doesn't already exist in the registry. In the second example, we're unregistering any definition that has the "SHIFT+S" key combination defined. Both examples prevent a duplicate registration which you may find useful in applications where users can define their own keyboard shortcuts (you wouldn't want user shortcuts overriding system shortcuts, would you?).

Our Updated Keyman Object Looks Like This

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

    getTarget: function(e) {
        e = window.event || e;
        var target = e.target || e.srcElement;
        return target;
    },

    indexOf: function(id_or_key, modifiers) {
        id_or_key = id_or_key.toUpperCase(); //all keys are converted to UpperCase
        var index = -1;
    
        for(i = 0; i < this.registry.length; i++) {
            if(this.registry[i][0] == id_or_key || this.registry[i][1] == id_or_key) {
                if(modifiers) {
                    if(this.registry[i][3].join(',') == modifiers) {
                        index = i;
                        break;
                    }
                } else {
                    index = i;
                    break;
                }
            }
        }

        return index;
    },

    press: function(e) {
        var target = this.getTarget(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');

        var tags = ['INPUT','SELECT','TEXTAREA'];

        if(tags.inArray(target.tagName)) {
            return this;   
        }

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

    unregister: function(id_or_key, modifiers) {
        var index = this.indexOf(id_or_key, modifiers);

        if(index > 0) {
            this.registry.splice(index, 1);
            return true;
        } else {
            return false;
        }
    }
};

To see the updated Keyman object, in action, check out the demonstration on How To Use Keyboard Shortcuts For Good and Not Evil.

Common Problems For Using Keyboard Shortcuts In Your Web Application

The biggest problem you are likely to face is keyboard shortcuts that have already been reserved by the browser. Registering a keyboard shortcut that is already reserved by the browser could have unexpected results. My recommendation is to use a single key for the shortcut or use SHIFT + key. Of course you can still use CTRL or ALT, but make sure the shortcut you are registering isn't already reserved by any of the more popular browsers.

Another problem that might come up is when a field on a form has focus without the user knowing. Then when the user tries to activate a keyboard shortcut, nothing happens. To combat this, either allow the shortcut event to run by taking out the code that halts it, or alert the user of the field that currently has focus by highlighting it in some way.

Using Keyboard Shortcuts for Good and not Evil

Whatever you do, don't detract from the user's experience! Keyboard shortcuts are meant to enhance the experience, not to make it frustrating. Use keyboard shortcuts for good, not evil!

Questions? Comments? Suggestions?

Feel free to leave a comment, questions or suggestions in regards to implementing keyboard shortcuts in your web application. Happy coding!