I wanted to do this as a way to add keyboard accelerators, mainly for enter and escape when doing editing etc.
The code was already implemented to be driven from mouse click events, so causing keyboard events to soft click particular target buttons for save and cancel was convenient as this was then completely isolated from the rest of the code, which only had to be concerned with being button driven.
I found the following issues during the implementation:-
A keypress event was fine for trapping Enter, but it did not trap escape correctly. For escape, I needed to drive from the keyup event.
My enter event was bubbling up and causing unwanted navigation. I initially tried cancelling bubbling and propagation as detailed here on Quirksmode.org:-
function doSomething(e)
{
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) e.stopPropagation();
}
This involves catering for browser differences , as shown, as the Microsoft/IE model uses cancelBubble, whereas the W3C model uses stopPropagation. In my case, this did not solve the issue.
My simple solution was for the event handler to return false when it had trapped and handled a keyclick such as enter, and to return true otherwise. Returning false blocked any further action on the event, and was also a standard mechanism. I did not bottom out the actual problem in my case, but this solved it completely.
When implementing the solution, I used the following technique:-
- An object literal was created which did a reverse map from all the key codes I might want to trap, to a mnemonic name for them (such as enter, escape, leftArrow).
- My event handler call accepts a keyActions object literal argument where the names are the above mnemonics, and the values are the element IDs which need to be clicked when the matching key code is detected.
- In the event handler, I reverse lookup the key code to the mnemonic, and then look up the element Id in the passed object literal using the mnemonic.
- If I find an element Id, I click it and return false. If I do not find one, I return true as I am not trapping that key.
- I also try a lookup on the passed keyActions object using the raw key code. This means that the caller can either use a mnemonic or a key code to specify what key is handled.
- The event handler therefore has very little code, and does not need any loops – it just does 2 associative lookups to get the target element to click.
- As an additional feature, I allow an idPath key/value to be passed in keyActions. If this is present, any element value which is not prefixed with ‘#’ will be prefixed with the id path, to simplify the call site where a number of elements with a common ID prefix are specified. Elements already prefixed with ‘#’ are left untouched.
This technique would also work well when implementing global key handling attached to the <body> tag. In this case, it may be desirable to switch the global handling depending on what is open on the page. For example, there might be an in-page detail form which is opened in-place and client side using a Primefaces p:panel. In this case, the keyActions object could be switched when the detail panel is opened, and switched back when it is closed.
Another useful trick if the complexity warrants it could be to implement a stack such that detail panels on the page push their own keyActions onto the stack, perhaps inheriting the global ones and modifying them as required. When a detail level is closed, it can just pop the stack and does not need to know about the higher levels of actions on the page. To assist with this, it would be useful to be able to clone the previous level actions when adding a new level. Prototypes could be used for this, but they need constructor functions etc. and do not map well to our simple case which just uses JSON like object literals. The jQuery extend method could be a good choice for this, and is detailed here on StackOverflow by none other than Mr jQuery himself, John Resig (!)
The following example code fragment from a utility object illustrates the idea:-
var uk=uk||{}; uk.co=uk.co||{}; uk.co.salientsoft=uk.co.salientsoft||{};
uk.co.salientsoft.util= {
clearElement: function(elementId) {
jQuery(uk.co.salientsoft.util.escapeId(elementId)).val(”);
},
escapeId: function(elementId) {
//a value tests true if it is non-null, non-blank, not NAN, not undefined, and not false
var escapedId = (elementId) ? ((elementId.charAt(0) == ‘#’ ? ” : ‘#’) + elementId.replace(/:/g, ‘\\3A ‘)) : ”;
return escapedId;
},
keyCodeNames: {
/*
* Common list of keycodes, allowing reverse lookup of name from code.
* Derived from: http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
* This only includes the common function/special keys which should be browser independant.
*/
8:’backspace’,
9:’tab’,
13:’enter’,
16:’shift’,
17:’ctrl’,
18:’alt’,
27:’escape’,
33:’pageUp’,
34:’pageDown’,
35:’end’,
36:’home’,
37:’leftArrow’,
38:’upArrow’,
39:’rightArrow’,
40:’downArrow’,
45:’insert’,
46:’delete’,
112:’f1′,
113:’f2′,
114:’f3′,
115:’f4′,
116:’f5′,
117:’f6′,
118:’f7′,
119:’f8′,
120:’f9′,
121:’f10′,
122:’f11′,
123:’f12’
},
clickOnKey: function(event, keyActions) {
/*
* This is a keyup/keydown/keypress event handler.
* keyActions is an object literal containing key mnemonics for the keys, and target elements for the values.
* As well as mnemonics, raw key code numbers can be used.
* A special object literal key, idPath, may be used to specify a default ID prefix.
* If present, it is applied to all element IDs which are not prefixed with a ‘#’.
* Examples:-
* onkeypress="return uk.co.salientsoft.util.clickOnKey(event, {enter:’frm1:cmdSaveNew:link’});"
* onkeyup="return uk.co.salientsoft.util.clickOnKey(event, {escape:’cmdCancelNew:link’, idPath:’frm1:’});"
*
* NOTE – escape handling does not work on a keypress event.
* The event is detected, but the click fails to do anything.
* onkeyup should be used for escape, as this does not suffer from the problem.
*/
var keycode = (event.keyCode ? event.keyCode : event.which);
/*
* Try to fetch the target element id from the passed keyActions assuming a string key code name was used, e.g. ‘enter’
* If this fails, try a lookup based on the raw key code.
* Doing it this way avoids looping.
*/
var actionElement = keyActions[uk.co.salientsoft.util.keyCodeNames[keycode]];
if (actionElement == null) {
actionElement = keyActions[keycode];
}
/* If found a target for the event keycode, click it
* and return false to stop any propagation, else return true
*/
if (actionElement != null) {
if (actionElement.charAt(0) != ‘#’ && keyActions.idPath != null) {
actionElement = keyActions.idPath + actionElement;
}
jQuery(uk.co.salientsoft.util.escapeId(actionElement)).click();
return false;
}
return true;
}
};