[JavaScript] Website shortcuts


JavaScript Shortcuts

Ever wondered to have personalised shortcuts in your website? It's not that hard, here is how you can do that...

First of all: there are tons of libraries that do this the same, or possibly better. So, if you're just looking for a copy/paste code, this is not for you. In this article I'll show you how onkeydown and onkeyup events work in JavaScript.

JavaScript is both powerful and a curse, if not used wisely: it's not uncommon to see wonderful websites that literally can't be viewed by old computers or mobile devices. So always remember: never do things in JavaScript unless you can't do otherwise. This doesn't mean you should keep all the work server-side, because maybe you want to keep things light to allow more users access your website without letting the server overload. Also if you can do the same things in CSS, HTML or JavaScript, always prefere the first and the second, as they're always faster.

Obviously you can't work on key bindings server-side, in CSS or in HTML. So this has to be done in JavaScript. Last but not least, if the user disables JavaScript, the bindings won't work.


Attention: this work is under the Creative Commons Attribution 3.0 Unported licence


Live demo: preview, code

shortcuts.zip (contains the shortcuts class).


The HTML part is at your will, as this is all about JavaScript. We'll write a JavaScript class which will be reused at will in any project you're working on.

The idea is to provide a very simple shortcut system (single characters, or [shift|ctrl|alt]+character). You can easily extend this once you'll have understood how the system works.


Class definition and private variables:

:::JavaScript
function shortcuts() {
    var keyCodes = {"shift": 16, "ctrl": 17, "alt": 18};
    var definedShortcuts = [];

keyCodes is used to keep modifier key codes. When a key is being pressed, it's passed to the handler via an event object, which doesn't have the character's symbol, but only its Unicode digit. Yes: Unicode, not UTF-8. If you want to support UTF-8 (or UTF-16), you'll have to work it out a little bit (you can find several libraries that do this for you).

The defined shortcuts variable is an array and... oh well, I think it self-explains. We'll fill it later with a proper method (addShortcut).

:::JavaScript
    this.addShortcut = function (shortcut, func) {
        var insert = {'mod': [], 'char': undefined, 'func': func};

        var orig = shortcut;

        shortcut = shortcut.toLowerCase().split("+");

        for(i in shortcut) {
            if(keyCodes[shortcut[i]])
                insert.mod.push(shortcut[i]);
            else
                insert.char = shortcut[i].toUpperCase().charCodeAt(0);
        }

        if(!insert.char || !func)
            return;

        insert.func = func;

        definedShortcuts.push(insert);
        console.log("Shortcut " + orig + " succesfully added");
    };

And here it is: this method will fill our definedShourtcuts private variable. The idea is to let the library-user easily add shortcuts just in the common way people write about shortcuts, for example "CTRL+X". This method is case-insensitive, so it's not a problem if the same shortcut is called "Ctrl+x" or whatever. If you want, you can strip out whitespace to make it more dumb-proof.

The method accepts two arguments: the shortcut as we've just talked about, and a function, which will be called when the shortcut is being pressed.

The insert variable is the object we'll push into the defined shortcuts class variable. The first thing then is to define its basic structure.

Secondly we keep a copy of the shortcut we're going to elaborate, but just for the ending log. If you wish, you can simply strip off both for keeping the code cleaner.

Now it's time to analyse the shortcut: we want to transform from something like modifier+[...]+character into something manageable in JavaScript (hint: the insert variable). So we need to split (PHP users: explode) it by the plus operand. Because we're making it case-insensitive, we first transform it lowercase.

We've got an array of strings now, so we need to iterate them to differentiate between modifiers and the actual character. Apparently browsers, or at least Chrome with which I've tested the code, will send the upper case variant of the character pressed (i.e. if you press e, it will send E), so in case it's the character and not a modifier, we need to transform it upper case, then we save its key code (because we're going to work with this in the key event).

The last lines are just some checks. Eventually, if it's all ok, the shortcut and the relative function are being stored in our shortcuts array.

:::JavaScript
    this.startListener = function (elem) {
        elem.onkeyup = keyUpHandler;
        elem.onkeydown = keyDownHandler;
    };

This is the second and last public class' method: it simply attaches the key event handlers to the onKeyUp and onKeyDown events. The former event must be handled because otherwise shortcuts like CTRL+S could interfere with the default shortcuts, the latter will actually call the relative function.

:::JavaScript
    function keyDownHandler(e) {
        for(i in definedShortcuts) {
            var sc = definedShortcuts[i];
            var shift = sc.mod.indexOf('shift') != -1;
            var ctrl = sc.mod.indexOf('ctrl') != -1;
            var alt = sc.mod.indexOf('alt') != -1;


            if(sc.char != e.keyCode
                || (shift ^ e.shiftKey)
                || (ctrl ^ e.ctrlKey)
                || (alt ^ e.altKey))
                continue;

            e.preventDefault();
            break;
        }
    }

This is the handler which will block any default behavior if the shortcut has been defined in our class. The key event handlers accept one parameter: the key event itself, which contains useful information about which keys have been pressed (or released, or whatever). The idea is to iterate through all the defined shortcuts and search for the one which matches the keys pressed.

You may ask about the ^ operator: it's the XOR (eXclusive OR). If you don't know what a XOR is, it's simply an OR which will return 0 if both the operands are true.

a ^ b | result
0 ^ 0 | 0
0 ^ 1 | 1
1 ^ 0 | 1
1 ^ 1 | 0

For example, the first XOR is equal to shift && !e.shiftKey || !shift && e.shiftKey.

e.preventDefault() is the key of this method: because the handler is executed before the standard browser functions, you're able to stop them before they even start. In this way you can safely bind any combination of modifiers and characters without worrying about browser-specific shortcuts.

:::JavaScript
    function keyUpHandler(e) {
        if(e.keyCode == keyCodes.shift || e.keyCode == keyCodes.ctrl || e.keyCode == keyCodes.alt)
            return;

        for(i in definedShortcuts)
        {
            var sc = definedShortcuts[i];
            var shift = sc.mod.indexOf('shift') != -1;
            var ctrl = sc.mod.indexOf('ctrl') != -1;
            var alt = sc.mod.indexOf('alt') != -1;


            if(sc.char != e.keyCode
                || (shift ^ e.shiftKey)
                || (ctrl ^ e.ctrlKey)
                || (alt ^ e.altKey))
                continue;

            sc.func();
            break;
        }
    }

This is similar to the keyDownHandler. I've added a check to interrupt the method earlier if the user is pressing modifier keys (as they can't be alone a shortcut). The cycle is the same as the previous one, but instead of preventing the default behavior, it will execute the function defined in the addShortcut method's second parameter.


Easy, isn't it?

Now, an example on how to use the class:

:::JavaScript
// instantiate a new shortcuts object
var sc = new shortcuts();

// add a new shortcut
sc.addShortcut("Ctrl+Alt+9", function() {
    console.log("Hello buddy!");
});

// overwrite the default "save" shortcut
sc.addShortcut("Ctrl+S", function() {
    console.log("Alternative saving function");
});

// attach the handlers on the main window
sc.startListener(window);

- 17th August 2013

> back