Skip to content

Rethinking $.keymap

Download the code.

See the demo.

See the hotkeys demo.

Dealing with keydown events is a pain, since the event object has always encoded the key as an arbitrary numeric code, so any program that deals with key-related events has something like:

var charcodes = {
	 32	: ' ',
	  8	: 'Backspace',
	 20	: 'CapsLock',
	 46	: 'Delete',
	 13	: 'Enter',
	 27	: 'Escape',
	 45	: 'Insert',
	144	: 'NumLock',
	  9	: 'Tab'
};

And so on. There is a proposal to add a key field that uses standardized names for the keys, but only Firefox and IE implement that right now. The time is ripe for a polyfill to bring Chrome and Safari into the twenty-first century, and I'd already worked on something similar with my $.keymap plugin.

So I updated that plugin to simply implement the key field in keydown and keyup events. Now just include the plugin and you can do things like:

$('textarea').keydown(function(event){
  if (event.key == 'Escape') { do something useful }
});

I also added a nonstandard field called keymap that includes the state of the modifier keys, using Microsoft's sendkeys notation, with + for shift, ^ for control and % for alt. While Windows doesn't have a meta key, it's part of the standard key event, and I included that with ~ for meta. I can't test it, though, so feedback on whether it works would be nice. So now rather than doing:

$('textarea').keydown(function(event){
  if (event.key == 'Enter' && event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey) { do something useful }
});

you can do:

$('textarea').keydown(function(event){
  if (event.keymap == '^+Enter') { do something useful }
});

There's one exception: the standard uses ' ' for the key value for the space bar, where IE uses Spacebar. I think the latter is more useful, since I can't see the space character (and I want to use space as a delimiter). So the key field is ' ', per the standard, but keymap use Spacebar.

It will normalize synthetic keystrokes too, so doing:

$(el).trigger({type: 'keydown', keymap: '^%a'});

or

$(el).trigger({type: 'keydown', key: 'a', ctrlKey: true, altKey: true});

allows you to do

$(el).on('keydown', function (event){
  if (event.key == 'a' && event.ctrlKey && event.altKey) {do something useful}
});

or simply

$(el).on('keydown', function (event){
  if (event.keymap == '^%a') {do something useful}
});

The keymap passed as part of a synthetic event will be normalized to the standard key names, as will the key field. Indicate shifted letters with uppercase; thus control-a is '^a' and control-shift-a is '^A', not '+^a'. Redundant shifts are removed: '+@' is normalized to '@'. It also allows for VIM and jquery.hotkeys notation; thus both '<C-a> and 'ctrl+a' are normalized to '^a'. Please look at the source for aliasgenerator for all the translations.

Hotkeys

Writing if (event.keymap = '^%a') { ... gets old, and I wanted to be able to look for sequences of characters as well (Escape % if I'm trying to implement emacs). John Resig wrote a hotkeys plugin and I added keyup and keydown handlers to implement the same idea. Add a keys field to the data argument and the handler will only be called if the keymap of that event matches it (it should be called keymaps rather than keys, for consistency. I'm keeping it this way). Use a space-delimited list of keys to match a sequence (and use Spacebar to match the space key). The keys field will be normalized as above, so any notation works. The Event object will have an additional field, hotkeys, which is the sequence of keys matched.
Thus:

$(el).on('keydown', {keys: '^%a'}, handler);
$(el).off('keydown', {keys: '^%a'}); // works
$(el).off('keydown', handler); // DOES NOT WORK. The plugin changes the handler internally so the remove mechanism can't search for it.

$('body').on('keydown', {
    keys: 'up up down down left right left right b a' // This will be normalized to ArrowUp ArrowUp ArrowDown ArrowDown ArrowLeft ArrowRight ArrowLeft ArrowRight b a
  }, 
  function() {alert('Konami!')}
);

$('body').on('keydown', 'input', {keys: '^A'}, function() {alert('Control A was pressed in an input')} ); // selectors work

$(el).keydown ({keys: 'C-x C-s'}, function(event){ // $.keydown is just a shortcut for $.on('keydown'...
  // do emacs save-buffer, somehow...

  console.log (event.hotkeys); // displays '^x ^s', the "normalized" form of the keystrokes
});

You can also use a regular expression to match keys, but that has to match the final normalized keystrokes, including exactly one space between keys:

$(el).keydown({keys: /" [a-z] p/}, function (event){
  // the vi put command
  var buffer = event.hotkeys.charAt(1); // the letter matching [a-z] above
  // do the put command
});

There's one other option: allowDefault. Normally if a sequence is in the process of being matched, the intermediate keystrokes are discarded. Set allowDefault to true to pass them through. So, for the Konami code you would still want the arrow keys to work:

$('body').on('keydown', {
    keys: '{up} {up} {down} {down} {left} {right} {left} {right} b a', // braces notation from Microsoft's sendkeys will be normalized
    allowDefault: true // the arrow keys will still work
  }, 
  function() {alert('Konami!')}
);

Events

The hotkeys plugin also triggers two other events: 'keymapprefix' when the event is the prefix of a valid sequence, and 'keymapcomplete' when a sequence is matched. Both are sent with an additional parameter indicating what keys were pressed so far (in the normalized notation). So:

$('body').on('keymapprefix keymapcomplete', function(event, keys){
	alert ('sequence pressed: '+keys);
})

Localization

The code as it stands assumes a standard US-English keyboard. There's no way for javascript code to know what keyboard the user has. This is bad in browsers that do not support key directly, since all I get is the keyCode, and that corresponds to a different character in every keyboard. But even in browsers that support key natively, I still need to know whether the key should be marked with a + when shifted; for instance, '@' with shiftKey set should have a keymap of @ (no +, since that character implies the shift), but the 'DEL' key with shiftKey set should have a keymap of +Delete. This is even more of a problem with the AltGr key, which in most keyboards does not change the printed character and thus needs to be indicated in keymap.

As far as I can tell, there are two modifier keys that affect the printed character: shift and AltGr. In all modern browsers, AltGr is indicated by setting both ctrlKey and altKey, so keymap indicates it with ^%; so AltGr and the A key produces ^%a.

You can change the assumed keyboard layout with $.keymap.setlayout(layout), where layout is a object of the form:

{
  normal: "`1234567890-=\\qwertyuiop[]asdfghjkl;'zxcvbnm,./",
  shift: ... // keyboard layout for shifted keys
  alt: ... // keyboard layout for the AltGr key
  shift_alt: ... // keyboard layout for Shift and AltGr key both held down
}

Each string should have 47 characters, just the rows of the keyboard left to right and top to bottom (it assumes the keyboard that has an extra-large Enter key and the backslash to the right of '=', not right of ']'). The format is that used by Virtual Keyboard, which I use extensively. Unfortunately, the development site seems to be dead, but you can download the source from SourceForge and look in the layouts folder for layout formats for lots of different languages.

Keys that do not change with the modifier should be indicated with '\00', or use an abbreviated format: an object {index1: substring1, index2: substring2 ...} where index is the starting index of the given substring. For all keys that have an uppercase, if the shift string is not set, it will be set to c.toUpperCase(). Any of the strings may be omitted, and will be assumed to be all blank. For example:

var layouts = {
	US :{
		normal:'`1234567890-=\\qwertyuiop[]asdfghjkl;\'zxcvbnm,./',
		shift:{0:'~!@#$%^&*()_+|',24:'{}',35:':"',44:'<>?'} // note the uppercase letters are skipped; the plugin can figure that out for itself
	},
	UK :{
		normal:'`1234567890-=#qwertyuiop[]asdfghjkl;\'zxcvbnm,./',
		shift:{0:'¬!"£$%^&*()_+~',24:'{}',35:':@',44:'<>?'},
		alt:{0:'¦',4:'€',16:'é',20:'úíó',26:'á'} // altGr keys
	},
	Israel : {
		normal:';1234567890-=\\/\'קראטוןםפ][שדגכעיחלךף,זסבהנמצתץ.',
		shift:{0:'~!@#$%^&*)(_+|QWERTYUIOP}{ASDFGHJKL:"ZXCVBNM><'+'?'}, // I had to split the < and ? up to avoid confusing my syntax highlighter
		alt:{3:'€₪°ֽ֫×‎‏',11:'‏־–ֻׂ',16:'ֳָ',19:'װֹ',23:'ְֲַּֿ',30:'ױײִ',34:'”„״',43:'ֵ’‚÷'}
	}
};
$.keymap.setlayout(layouts.Israel);

Utilities

$.keymap(event) is the basic plugin; it normalizes the Event object as described (setting key and keymap) and returns the value of keymap. Possibly useful if you are handling events straight with addEventListener(); the plugin patches the jQuery handing to automatically do this.

$.keymap.normalize(event) is the same as $.keymap(event) but returns event.

$.keymap.normalizeString(string) is the workhorse function to normalize the key descriptions; $.keymap.normalizeString('ctrl-esc') returns '^Escape'.

$.keymap.normalizeList(string) does the same for a space-delimited list of key descriptions; $.keymap.normalizeList('ctrl-esc % alt-5') returns '^Escape % %5'.

Post a Comment

Your email is never published nor shared. Required fields are marked *