Section 17.5. Key Events


17.5. Key Events

As you've already learned, events and event handling are subject to many browser incompatibilities. And key events are more incompatible than most: they are not standardized in the DOM Level 2 Events module, and IE and Mozilla-based browsers treat them somewhat differently. Unfortunately, this simply reflects the state of the art in keyboard input handling. The OS and windowing system APIs upon which browsers are built are typically complex and confusing. Text input handling is tricky on many levels, from hardware keyboard layouts to input method processing for ideographic languages.

Despite the difficulties, useful key event-handling scripts can be written to work in at least Firefox and IE. This section demonstrates a couple of simple scripts and then presents a more general Keymap class for mapping keyboard events to JavaScript handler functions.

17.5.1. Types of Key Events

There are three keyboard event types: keydown, keypress, and keyup; they correspond to the onkeydown, onkeypress, and onkeyup event handlers. A typical keystroke generates all three events: keydown, keypress, and then keyup when the key is released. If a key is held down and autorepeats, there may be multiple keypress events between the keydown and keyup, but this is OS- and browser-dependent and cannot be counted on.

Of the three event types, keypress events are the most user-friendly: the event object associated with them contains the encoding of the actual character generated. The keydown and keyup events are lower-level; their key events include a "virtual keycode" that is related to the hardware codes generated by the keyboard. For alphanumeric characters in the ASCII character set, these virtual keycodes match the ASCII codes, but they are not fully processed. If you hold down the Shift key and press the key labeled 2, the keydown event will tell you that the keystroke "shift-2" has occurred. The keypress event will interpret this for you and let you know that that keystroke produced the printable character "@". (This mapping may be different with different keyboard layouts.)

Nonprinting function keys, such as Backspace, Enter, Escape, the arrow keys, Page Up, Page Down, and F1 through F12 generate keydown and keyup events. In some browsers, they also generate keypress events. In IE, however, keypress events occur only when the keystroke has an ASCII codethat is, when it is a printing character or a control character. Nonprinting function keys have virtual keycodes as printing keys do, and these are available through the event object associated with the keydown event. For example, the left arrow key generates a keycode of 37 (at least using a standard U.S. keyboard layout).

As a general rule of thumb, the keydown event is most useful for function keys, and the keypress event is most useful for printing keys.

17.5.2. Key Event Details

The event objects passed to keydown, keypress, and keyup event handlers are the same for each type of event, but the interpretation of certain properties of those objects depends on the event type. The event objects are browser-dependent, of course, with different properties in Firefox and IE.

If the Alt, Ctrl, or Shift keys are held down when a key is pressed, this is indicated by the state of the altKey, ctrlKey, and shiftKey properties of the event object. These properties are actually portable: they work in both Firefox and IE, and for all key event types. (One exception: Alt key combinations are considered nonprinting in IE, so they don't generate a keypress event.)

Getting the keycode or character code of a key event is less portable, however. Firefox defines two properties. keyCode holds the low-level virtual keycode of a key and is sent with the keydown event. charCode holds the encoding of the printable character generated by pressing that key and is sent with the keypress event. In Firefox, function keys generate a keypress event; in this case, charCode is zero, and the keyCode contains the virtual keycode.

In IE, there is only the keyCode property, and its interpretation depends on the type of event. For keydown events, keyCode is a virtual keycode; for keypress events, keyCode is a character code.

Character codes can be converted to characters with the static function String.fromCharCode( ). To process keycodes correctly, you must simply know which keys generate which keycodes. Example 17-6 at the end of this section includes a mapping of keycodes to the function keys they represent (on a standard U.S. keyboard layout, at least).

17.5.3. Filtering Keyboard Input

Key event handlers can be used with <input> and <textarea> elements to filter the user's input. For example, suppose you want to force the user's input to uppercase:

 Surname: <input  type="text"                 onkeyup="this.value = this.value.toUpperCase( );"> 

An <input> element appends the typed character to its value property after the keypress event occurs. By the time the keyup event arrives, therefore, the value property has been updated, and you can simply convert the entire thing to uppercase. This works no matter where the cursor is positioned within the text field, and you can accomplish it using the DOM Level 0 event model. You do not need to know what key was pressed, and so you do not need to access the event object associated with the event. (Note that the onkeyup event handler is not triggered if the user pastes text into the field using the mouse. To handle that case, you'd probably also want to register an onchange handler. See Chapter 18 for further details about form elements and their event handlers.)

A more complex example of key event filtering uses the onkeypress handler to restrict the user's input to a certain subset of characters. You might want to prevent a user from entering letters into a field intended for numeric data, for example. Example 17-5 is an unobtrusive module of JavaScript code that allows exactly this sort of filtering. It looks for <input type=text> tags that have an additional (nonstandard) attribute named allowed. The module registers a keypress event handler on any such text field to restrict input to characters that appear in the value of the allowed attribute. The initial comment at the top of Example 17-5 includes some sample HTML that uses the module.

Example 17-5. Restricting user input to a set of characters

 /**  * InputFilter.js: unobtrusive filtering of keystrokes for <input> tags  *  * This module finds all <input type="text"> elements in the document that  * have a nonstandard attribute named "allowed". It registers an onkeypress  * event handler for any such element to restrict the user's input so that  * only characters that appear in the value of the allowed attribute may be  * entered. If the <input> element also has an attribute named "messageid",  * the value of that attribute is taken to be the id of another document  * element. If the user types a character that is not allowed, the messageid  * element is made visible. If the user types a character that is allowed,  * the messageid element is hidden. This message id element is intended to  * offer an explanation to the user of why her keystroke was rejected. It  * should typically be styled with CSS so that it is initially invisible.  *  * Here is some sample HTML that uses this module.  *   Zipcode:  *   <input  type="text" allowed="0123456789" message>  *   <span  style="color:red;visibility:hidden">Digits only</SPAN>  *  * In browsers such as IE, which do not support addEventListener( ), the  * keypress handler registered by this module overwrites any keypress handler  * defined in HTML.  *  * This module is purely unobtrusive: it does not define any symbols in  * the global namespace.  */ (function( ) {  // The entire module is within an anonymous function     // When the document finishes loading, call the init( ) function below     if (window.addEventListener) window.addEventListener("load", init, false);     else if (window.attachEvent) window.attachEvent("onload", init);     // Find all the <input> tags we need to register an event handler on     function init( ) {         var inputtags = document.getElementsByTagName("input");         for(var i = 0 ; i < inputtags.length; i++) { // Loop through all tags             var tag = inputtags[i];             if (tag.type != "text") continue; // We only want text fields             var allowed = tag.getAttribute("allowed");             if (!allowed) continue;  // And only if they have an allowed attr             // Register our event handler function on this input tag             if (tag.addEventListener)                 tag.addEventListener("keypress", filter, false);             else {                 // We don't use attachEvent because it does not invoke the                 // handler function with the correct value of the this keyword.                 tag.onkeypress = filter;             }         }     }     // This is the keypress handler that filters the user's input     function filter(event) {         // Get the event object and character code in a portable way         var e = event || window.event;         // Key event object         var code = e.charCode || e.keyCode;    // What key was pressed         // If this keystroke is a function key of any kind, do not filter it         if (e.charCode == 0) return true;       // Function key (Firefox only)         if (e.ctrlKey || e.altKey) return true; // Ctrl or Alt held down         if (code < 32) return true;             // ASCII control character         // Now look up information we need from this input element         var allowed = this.getAttribute("allowed");     // Legal chars         var messageElement = null;                      // Message to hide/show         var messageid = this.getAttribute("messageid"); // Message id, if any         if (messageid)  // If there is a message id, get the element             messageElement = document.getElementById(messageid);         // Convert the character code to a character         var c = String.fromCharCode(code);         // See if the character is in the set of allowed characters         if (allowed.indexOf(c) != -1) {             // If c is a legal character, hide the message, if any             if (messageElement) messageElement.style.visibility = "hidden";             return true; // And accept the character         }         else {             // If c is not in the set of allowed characters, display message             if (messageElement) messageElement.style.visibility = "visible";             // And reject this keypress event             if (e.preventDefault) e.preventDefault( );             if (e.returnValue) e.returnValue = false;             return false;         }     } })( ); // Finish anonymous function and invoke it. 

17.5.4. Keyboard Shortcuts with a Keymap

Graphical programs that run on a user's desktop typically define keyboard shortcuts for commands that are also accessible through pull-down menus, toolbars, and so on. Web browsers (and HTML) are quite mouse-centric, and web applications do not, by default, support keyboard shortcuts. They should, however. If you use DHTML to simulate pull-down menus for your web application, you should also support keyboard shortcuts for those menus. Example 17-6 shows how you might do this. It defines a Keymap class that maps from keystroke identifiers such as "Escape", "Delete", "Alt_Z", and "alt_ctrl_shift_F5" to JavaScript functions that are invoked in response to those keystrokes.

Pass key bindings to the Keymap( ) constructor in the form of a JavaScript object in which property names are keystroke identifiers and property values are handler functions. Add and remove bindings with the bind( ) and unbind( ) methods. Install a Keymap on an HTML element (often the Document object) with the install( ) method. Installing a keymap on an element registers both onkeydown and onkeypress event handlers on that element in order to capture both function keys and printable characters.

Example 17-6 begins with a long comment that explains the module in more detail. See especially the section of this comment titled "Limitations."

Example 17-6. A Keymap class for keyboard shortcuts

 /*  * Keymap.js: bind key events to handler functions.  *  * This module defines a Keymap class. An instance of this class represents a  * mapping of key identifiers (defined below) to handler functions. A  * Keymap can be installed on an HTML element to handle keydown and keypress  * events. When such an event occurs, the Keymap uses its mapping to invoke  * the appropriate handler function.  *  * When you create a Keymap, pass a JavaScript object that represents the  * initial set of bindings for the Keymap. The property names of this object  * are key identifers, and the property values are the handler functions.  *  * After a Keymap has been created, you can add new bindings by passing a key  * identifer and handler function to the bind( ) method.  You can remove a  * binding by passing a key identifier to the unbind( ) method.   *  * To make use of a Keymap, call its install( ) method, passing an HTML element,  * such as the document object. install( ) adds an onkeypress and onkeydown  * event handler to the specified object, replacing any handlers previously set  * on those properties. When these handlers are invoked, they determine the  * key identifier from the key event and invoke the handler function, if any,  * bound to that key identifier. If there is no mapping for the event, it uses  * the default handler function (see below), if one is defined. A single  * Keymap may be installed on more than one HTML element.  *  * Key Identifiers  *  * A key identifier is a case-insensitive string representation of a key plus  * any modifier keys that are held down at the same time. The key name is the  * name of the key: this is often the text that appears on the physical key of  * an English keyboard. Legal key names include "A", "7", "F2", "PageUp",  * "Left", "Delete", "/", "~". For printable keys, the key name is simply the  * character that the key generates. For nonprinting keys, the names are  * derived from the KeyEvent.DOM_VK_ constants defined by Firefox. They are  * simply the constant name, with the "DOM_VK_" portion and any underscores  * removed. For example, the KeyEvent constant DOM_VK_BACK_SPACE becomes  * BACKSPACE. See the Keymap.keyCodeToFunctionKey object in this module for a  * complete list of names.  *  * A key identifier may also include modifier key prefixes. These prefixes are  * Alt_, Ctrl_, and Shift_. They are case-insensitive, but if there is more  * than one, they must appear in alphabetical order. Some key identifiers that  * include modifiers include "Shift_A", "ALT_F2", and "alt_ctrl_delete". Note  * that "ctrl_alt_delete" is not legal because the modifiers are not in  * alphabetical order.  *  * Shifted punctuation characters are normally returned as the appropriate  * character. Shift-2 generates a key identifier of "@", for example. But if  * Alt or Ctrl is also held down, the unshifted symbol is used instead.  * We get a key identifier of Ctrl_Shift_2 instead of Ctrl_@, for example.  *  * Handler Functions  *  * When a handler function is invoked, it is passed three arguments:  *   1) the HTML element on which the key event occurred  *   2) the key identifier of the key that was pressed  *   3) the event object for the keydown event  *  * Default Handler  *  * The reserved key name "default" may be mapped to a handler function. That  * function will be invoked when no other key-specific binding exists.  *  * Limitations  *  * It is not possible to bind a handler function to all keys. The operating  * system traps some key sequences (Alt-F4, for example). And the browser  * itself may trap others (Ctrl-S, for example). This code is browser, OS,  * and locale-dependent. Function keys and modified function keys work well,  * and unmodified printable keys work well. The combination of Ctrl and Alt  * with printable characters, and particularly with punctuation characters, is  * less robust.  */ // This is the constructor function function Keymap(bindings) {     this.map = {};    // Define the key identifier->handler map     if (bindings) {   // Copy initial bindings into it, converting to lowercase         for(name in bindings) this.map[name.toLowerCase( )] = bindings[name];     } } // Bind the specified key identifier to the specified handler function Keymap.prototype.bind = function(key, func) {     this.map[key.toLowerCase( )] = func; }; // Delete the binding for the specified key identifier Keymap.prototype.unbind = function(key) {     delete this.map[key.toLowerCase( )]; }; // Install this Keymap on the specified HTML element Keymap.prototype.install = function(element) {     // This is the event-handler function     var keymap = this;     function handler(event) { return keymap.dispatch(event); }     // Now install it     if (element.addEventListener) {         element.addEventListener("keydown", handler, false);         element.addEventListener("keypress", handler, false);     }     else if (element.attachEvent) {         element.attachEvent("onkeydown", handler);         element.attachEvent("onkeypress", handler);     }     else {         element.onkeydown = element.onkeypress = handler;     } }; // This object maps keyCode values to key names for common nonprinting // function keys. IE and Firefox use mostly compatible keycodes for these. // Note, however that these keycodes may be device-dependent and different // keyboard layouts may have different values. Keymap.keyCodeToFunctionKey = {     8:"backspace", 9:"tab", 13:"return", 19:"pause", 27:"escape", 32:"space",     33:"pageup", 34:"pagedown", 35:"end", 36:"home", 37:"left", 38:"up",     39:"right", 40:"down", 44:"printscreen", 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",     144:"numlock", 145:"scrolllock" }; // This object maps keydown keycode values to key names for printable // characters. Alphanumeric characters have their ASCII code, but // punctuation characters do not. Note that this may be locale-dependent // and may not work correctly on international keyboards. Keymap.keyCodeToPrintableChar = {     48:"0", 49:"1", 50:"2", 51:"3", 52:"4", 53:"5", 54:"6", 55:"7", 56:"8",     57:"9", 59:";", 61:"=", 65:"a", 66:"b", 67:"c", 68:"d",     69:"e", 70:"f", 71:"g", 72:"h", 73:"i", 74:"j", 75:"k", 76:"l", 77:"m",     78:"n", 79:"o", 80:"p", 81:"q", 82:"r", 83:"s", 84:"t", 85:"u", 86:"v",     87:"w", 88:"x", 89:"y", 90:"z", 107:"+", 109:"-", 110:".", 188:",",     190:".", 191:"/", 192:"'", 219:"[", 220:"\\", 221:"]", 222:"\"" }; // This method dispatches key events based on the keymap bindings. Keymap.prototype.dispatch = function(event) {     var e = event || window.event;  // Handle IE event model     // We start off with no modifiers and no key name     var modifiers = ""     var keyname = null;     if (e.type == "keydown") {         var code = e.keyCode;         // Ignore keydown events for Shift, Ctrl, and Alt         if (code == 16 || code == 17 || code == 18) return;         // Get the key name from our mapping         keyname = Keymap.keyCodeToFunctionKey[code];         // If this wasn't a function key, but the ctrl or alt modifiers are         // down, we want to treat it like a function key         if (!keyname && (e.altKey || e.ctrlKey))             keyname = Keymap.keyCodeToPrintableChar[code];         // If we found a name for this key, figure out its modifiers.         // Otherwise just return and ignore this keydown event.         if (keyname) {             if (e.altKey) modifiers += "alt_";             if (e.ctrlKey) modifiers += "ctrl_";             if (e.shiftKey) modifiers += "shift_";         }         else return;     }     else if (e.type == "keypress") {         // If ctrl or alt are down, we've already handled it.         if (e.altKey || e.ctrlKey) return;         // In Firefox we get keypress events even for nonprinting keys.         // In this case, just return and pretend it didn't happen.         if (e.charCode != undefined && e.charCode == 0) return;         // Firefox gives us printing keys in e.charCode, IE in e.charCode         var code = e.charCode || e.keyCode;         // The code is an ASCII code, so just convert to a string.         keyname=String.fromCharCode(code);         // If the key name is uppercase, convert to lower and add shift         // We do it this way to handle CAPS LOCK; it sends capital letters         // without having the shift modifier set.         var lowercase = keyname.toLowerCase( );         if (keyname != lowercase) {             keyname = lowercase;    // Use the lowercase form of the name             modifiers = "shift_";   // and add the shift modifier.         }     }     // Now that we've determined the modifiers and key name, we look for     // a handler function for the key and modifier combination     var func = this.map[modifiers+keyname];     // If we didn't find one, use the default handler, if it exists     if (!func) func = this.map["default"];     if (func) {  // If there is a handler for this key, handle it         // Figure out what element the event occurred on         var target = e.target;              // DOM standard event model         if (!target) target = e.srcElement; // IE event model         // Invoke the handler function         func(target, modifiers+keyname, e);         // Stop the event from propagating, and prevent the default action for         // the event. Note that preventDefault doesn't usually prevent         // top-level browser commands like F1 for help.         if (e.stopPropagation) e.stopPropagation( );  // DOM model         else e.cancelBubble = true;                  // IE model         if (e.preventDefault) e.preventDefault( );    // DOM         else e.returnValue = false;                  // IE         return false;                                // Legacy event model     } }; 




JavaScript. The Definitive Guide
JavaScript: The Definitive Guide
ISBN: 0596101996
EAN: 2147483647
Year: 2004
Pages: 767

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net