19.2. Advanced Event Handling with DOM Level 2The event-handling techniques we've seen so far in this chapter are part of the Level 0 DOM: the de facto standard API that is supported by every JavaScript-enabled browser. The DOM Level 2 standard defines an advanced event-handling API that is significantly different (and quite a bit more powerful) than the Level 0 API. The Level 2 standard does not incorporate the existing API into the standard DOM, but there is no danger of the Level 0 API being dropped. For basic event-handling tasks, you should feel free to continue to use the simple API. The Level 2 DOM Events module is supported by Mozilla and Netscape 6, but is not supported by Internet Explorer 6. 19.2.1. Event PropagationIn the Level 0 event model, the browser dispatches events to the document elements on which they occur. If that object has an appropriate event handler, that handler is run. There is nothing more to it. The situation is more complex in the Level 2 DOM. In this advanced event model, when an event occurs on a Document node (known as the event target ), the target's event handler or handlers are triggered, but in addition, each of the target's ancestor nodes has one or two opportunities to handle that event. Event propagation proceeds in three phases. First, during the capturing phase, events propagate from the Document object down through the document tree to the target node. If any of the ancestors of the target (but not the target itself ) has a specially registered capturing event handler, those handlers are run during this phase of event propagation. (We'll learn how both regular and capturing event handlers are registered shortly.) The next phase of event propagation occurs at the target node itself: any appropriate event handlers registered directly on the target are run. This is akin to the kind of event handling provided by the Level event model. The third phase of event propagation is the bubbling phase, in which the event propagates or bubbles back up the document hierarchy from the target element up to the Document object. Although all events are subject to the capturing phase of event propagation, not all types of events bubble: for example, it does not make sense for a submit event to propagate up the document beyond the <form> element to which it is directed. On the other hand, generic events such as mousedown events can be of interest to any element in the document, so they do bubble up through the document hierarchy, triggering any appropriate event handlers on each of the ancestors of the target element. In general, raw input events bubble, while higher-level semantic events do not. (See Table 19-3, later in this chapter, for a definitive list of which events bubble and which do not.) During event propagation, it is possible for any event handler to stop further propagation of the event by calling the stopPropagation( ) method of the Event object that represents the event. We'll see more about the Event object and its stopPropagation( ) method later in this chapter. Some events cause an associated default action to be performed by the web browser. For example, when a click event occurs on an <a> tag, the browser's default action is to follow the hyperlink. Default actions like these are performed only after all three phases of event propagation complete, and any of the handlers invoked during event propagation have the opportunity to prevent the default action from occurring by calling the preventDefault( ) method of the Event object. Although this kind of event propagation may seem convoluted, it provides an important means of centralizing your event-handling code. The Level 1 DOM exposes all document elements and allows events (such as mouseover events) to occur on any of those elements. This means that there are many, many more places for event handlers to be registered than there were with the old Level 0 event model. Suppose you want to trigger an event handler whenever the user moves the mouse over a <p> element in your document. Instead of registering an onmouseover event handler for each <p> tag, you can instead register a single event handler on the Document object and handle these events during either the capturing or bubbling phase of event propagation. There is one other important detail about event propagation. In the Level 0 model, you can register only a single event handler for a particular type of event for a particular object. In the Level 2 model, however, you can register any number of handler functions for a particular event type on a particular object. This applies also to ancestors of an event target whose handler function or functions are invoked during the capturing or bubbling phases of event propagation. 19.2.2. Event Handler RegistrationIn the Level 0 API, you register an event handler by setting an attribute in your HTML or an object property in your JavaScript code. In the Level 2 event model, you register an event handler for a particular element by calling the addEventListener( ) method of that object. (The DOM standard uses the term "listener" in its API, but we'll continue to use the synonymous word "handler" in our discussion.) This method takes three arguments. The first is the name of the event type for which the handler is being registered. The event type should be a string that contains the lowercase name of the HTML handler attribute, with the leading "on" removed. Thus, if you use an onmousedown HTML attribute or onmousedown property in the Level 0 model, you'd use the string "mousedown" in the Level 2 event model. The second argument to addEventListener( ) is the handler (or listener) function that should be invoked when the specified type of event occurs. When your function is invoked, it is passed an Event object as its only argument. This object contains details about the event (such as which mouse button was pressed) and defines methods such as stopPropagation( ). We'll learn more about the Event interface and its subinterfaces later. The final argument to addEventListener( ) is a boolean value. If true, the specified event handler is used to capture events during the capturing phase of event propagation. If the argument is false, the event handler is a normal event handler and is triggered when the event occurs directly on the object or on a descendant of the element and subsequently bubbles up to the element. For example, you might use addEventListener( ) as follows to register a handler for submit events on a <form> element: document.myform.addEventListener("submit", function(e) { validate(e.target); } false); Or, if you wanted to capture all mousedown events that occur within a particular named <div> element, you might use addEventListener( ) like this: var mydiv = document.getElementById("mydiv"); mydiv.addEventListener("mousedown", handleMouseDown, true); Note that these examples assume that you've defined functions named validate( ) and handleMouseDown( ) elsewhere in your JavaScript code. Event handlers registered with addEventListener( ) are executed in the scope in which they are defined. They are not invoked with the augmented scope chain that is used for event handlers defined as HTML attributes. (See Section 19.1.6.) Because event handlers are registered in the Level 2 model by invoking a method rather than by setting an attribute or property, we can register more than one event handler for a given type of event on a given object. If you call addEventListener( ) multiple times to register more than one handler function for the same event type on the same object, all of the functions you've registered are invoked when an event of that type occurs on (or bubbles up to, or is captured by) that object. It is important to understand that the DOM standard makes no guarantees about the order in which the handler functions of a single object are invoked, so you should not rely on them being called in the order in which you registered them. Also note that if you register the same handler function more than once on the same element, all registrations after the first are ignored. Why would you want to have more than one handler function for the same event on the same object? This can be quite useful for modularizing your software. Suppose, for example, that you've written a reusable module of JavaScript code that uses mouseover events on images to perform image rollovers. Now suppose that you have another module that wants to use the same mouseover events to display additional information about the image (or the link that the image represents) in the browser's status line. With the Level API, you'd have to merge your two modules into one, so that they could share the single onmouseover property of the Image object. With the Level 2 API, on the other hand, each module can register the event handler it needs without knowing about or interfering with the other module. addEventListener( ) is paired with a removeEventListener( ) method that expects the same three arguments but removes an event handler function from an object rather than adding it. It is often useful to temporarily register an event handler and then remove it soon afterward. For example, when you get a mousedown event, you might register temporary capturing event handlers for mousemove and mouseup events so you can see if the user drags the mouse. You'd then deregister these handlers when the mouseup event arrives. In such a situation, your event-handler removal code might look as follows: document.removeEventListener("mousemove", handleMouseMove, true); document.removeEventListener("mouseup", handleMouseUp, true); Both the addEventListener( ) and removeEventListener( ) methods are defined by the EventTarget interface. In web browsers that support the Level 2 DOM Event API, all Document nodes implement this interface. For more information about these event-handler registration and deregistration methods, look up the EventTarget interface in the DOM reference section. One final note about event-handler registration: in the Level 2 DOM, event handlers are not restricted to document elements; you can also register handlers for Text nodes. In practice, however, you may find it simpler to register handlers on containing elements and allow Text node events to bubble up and be handled at the container level. 19.2.3. addEventListener( ) and the this KeywordIn the original Level 0 event model, when a function is registered as an event handler for a document element, it becomes a method of that document element (as discussed previously in Section 19.1.5). When the event handler is invoked, it is invoked as a method of the element, and, within the function, the this keyword refers to the element on which the event occurred. In Mozilla and Netscape 6, when you register an event handler function with addEventListener( ), it is treated the same way: when the browser invokes the function, it invokes it as a method of the document element for which it was registered. Note, however, that this is implementation-dependent behavior, and the DOM specification does not require that this happen. Thus, you should not rely on the value of the this keyword in your event handler functions when using the Level 2 event model. Instead, use the currentTarget property of the Event object that is passed to your handler functions. As we'll see when we consider the Event object later in this chapter, the currentTarget property refers to the object on which the event handler was registered but does so in a portable way. 19.2.4. Registering Objects as Event HandlersaddEventListener( ) allows us to register event handler functions. As discussed in the previous section, whether these functions are invoked as methods of the objects for which they are registered is implementation-dependent. For object-oriented programming, you may prefer to define event handlers as methods of a custom object and then have them invoked as methods of that object. For Java programmers, the DOM standard allows exactly this: it specifies that event handlers are objects that implement the EventListener interface and a method named handleEvent( ). In Java, when you register an event handler, you pass an object to addEventListener( ), not a function. For simplicity, the JavaScript binding of the DOM API does not require us to implement an EventListener interface and instead allows us to pass function references directly to addEventListener( ). If you are writing an object-oriented JavaScript program and prefer to use objects as event handlers, you might use a function like this to register them: function registerObjectEventHandler(element, eventtype, listener, captures) { element.addEventListener(eventtype, function(event) { listener.handleEvent(event); } captures); } Any object can be registered as an event listener with this function, as long as it defines a method named handleEvent( ). That method is invoked as a method of the listener object, and the this keyword refers to the listener object, not to the document element that generated the event. This function works because it uses a nested function literal to capture and remember the listener object in its scope chain. (If this doesn't make sense to you, you may want to review Section 11.4.) Although it is not part of the DOM specification, Mozilla 0.9.1 and Netscape 6.1 (but not Netscape 6.0 or 6.01) allow event listener objects that define a handleEvent( ) method to be passed directly to addEventListener( ) instead of a function reference. For these browsers, a special registration function like the one we just defined is not necessary. 19.2.5. Event Modules and Event TypesAs I've noted before, the Level 2 DOM is modularized, so an implementation can support parts of it and omit support for other parts. The Events API is one such module. You can test whether a browser supports this module with code like this: document.implementation.hasFeature("Events", "2.0") The Events module contains only the API for the basic event-handling infrastructure, however. Support for specific types of events is delegated to submodules. Each submodule provides support for a category of related event types and defines an Event type that is passed to event handlers for each of those types. For example, the submodule named MouseEvents provides support for mousedown, mouseup, click, and related event types. It also defines the MouseEvent interface. An object that implements this interface is passed to the handler function for any event type supported by the module. Table 19-2 lists each event module, the event interface it defines, and the types of events it supports. Note that the Level 2 DOM does not standardize any type of keyboard event, so no module of key events is listed here. Support for this type of event is expected in the DOM Level 3 standard. Table 19-2. Event modules, interfaces, and types
As you can see from Table 19-2, The HTMLEvents and MouseEvents modules define event types that are familiar from the Level 0 event module. The UIEvents module defines event types that are similar to the focus, blur, and click events supported by HTML form elements but are generalized so that they can be generated by any document element that can receive focus or be activated in some way. The MutationEvents module defines events that are generated when the document changes (is mutated) in some way. These are specialized event types and are not commonly used. As I noted earlier, when an event occurs, its handler is passed an object that implements the Event interface associated with that type of event. The properties of this object provide details about the event that may be useful to the handler. Table 19-3 lists the standard events again, but this time organizes them by event type, rather than by event module. For each event type, this table specifies the kind of event object that is passed to its handler, whether this type of event bubbles up the document hierarchy during event propagation (the "B" column), and whether the event has a default action that is cancelable with the preventDefault( ) method (the "C" column). For events in the HTMLEvents module, the fifth column of the table specifies which HTML elements can generate the event. For all other event types, the fifth column specifies which properties of the event object contain meaningful event details (these properties are documented in the next section). Note that the properties listed in this column do not include the properties that are defined by the basic Event interface, which contain meaningful values for all event types. It is useful to compare Table 19-3 with Table 19-1, which lists the Level 0 event handlers defined by HTML 4. The event types supported by the two models are largely the same (excluding the UIEvents and MutationEvents modules). The DOM Level 2 standard adds support for the abort, error, resize, and scroll event types that were not standardized by HTML 4, and it does not support the dblclick event type that is part of the HTML 4 standard. (Instead, as we'll see shortly, the detail property of the object passed to a click event handler specifies the number of consecutive clicks that have occurred.) Table 19-3. Event types
19.2.6. Event Interfaces and Event DetailsWhen an event occurs, the DOM Level 2 API provides additional details about the event (such as when and where it occurred) as properties of an object that is passed to the event handler. Each event module has an associated event interface that specifies details appropriate to that type of event. Table 19-2 (earlier in this chapter) lists four different event modules and four different event interfaces. These four interfaces are actually related to one another and form a hierarchy. The Event interface is the root of the hierarchy; all event objects implement this most basic event interface. UIEvent is a subinterface of Event: any event object that implements UIEvent also implements all the methods and properties of Event. The MouseEvent interface is a subinterface of UIEvent. This means, for example, that the event object passed to an event handler for a click event implements all the methods and properties defined by each of the MouseEvent, UIEvent, and Event interfaces. Finally, the MutationEvent interface is a subinterface of Event. The following sections introduce each of the event interfaces and highlight their most important properties and methods. You will find complete details about each interface in the DOM reference section of this book. 19.2.6.1. EventThe event types defined by the HTMLEvents module use the Event interface. All other event types use subinterfaces of this interface, which means that Event is implemented by all event objects and provides detailed information that applies to all event types. The Event interface defines the following properties (note that these properties, and the properties of all Event subinterfaces, are read-only):
In addition to these seven properties, the Event interface defines two methods that are also implemented by all event objects: stopPropagation( ) and preventDefault( ). Any event handler can call stopPropagation( ) to prevent the event from being propagated beyond the node at which it is currently being handled. Any event handler can call preventDefault( ) to prevent the browser from performing a default action associated with the event. Calling preventDefault( ) in the DOM Level 2 API is like returning false in the Level 0 event model. 19.2.6.2. UIEventThe UIEvent interface is a subinterface of Event. It defines the type of event object passed to events of type DOMFocusIn, DOMFocusOut, and DOMActivate. These event types are not commonly used; what is more important about the UIEvent interface is that it is the parent interface of MouseEvent. UIEvent defines two properties in addition to those defined by Event:
19.2.6.3. MouseEventThe MouseEvent interface inherits the properties and methods of Event and UIEvent and defines the following additional properties:
19.2.6.4. MutationEventThe MutationEvent interface is a subinterface of Event, and is used to provide event details for the event types defined by the MutationEvents module. These event types are not commonly used in DHTML programming, so details on the interface are not provided here. See the DOM reference section for details. 19.2.7. Example: Dragging Document ElementsNow that we've discussed event propagation, event-handler registration, and the various event object interfaces for the DOM Level 2 event model, we can finally look at how they work. Example 19-2 shows a JavaScript function, beginDrag( ) , that, when invoked from a mousedown event handler, allows a document element to be dragged by the user. beginDrag( ) takes two arguments. The first is the element that is to be dragged. This may be the element on which the mousedown event occurred or a containing element (e.g., you might allow the user to drag on the titlebar of a window to move the entire window). In either case, however, it must refer to a document element that is absolutely positioned using the CSS position attribute, and the left and top CSS attributes must be explicitly set to pixel values in a style attribute. The second argument is the event object associated with the triggering mousedown event. beginDrag( ) records the position of the mousedown event and then registers event handlers for the mousemove and mouseup events that will follow the mousedown event. The handler for the mousemove event is responsible for moving the document element, and the handler for the mouseup event is responsible for deregistering itself and the mousemove handler. It is important to note that the mousemove and mouseup handlers are registered as capturing event handlers, because the user can move the mouse faster than the document element can follow it, and some of these events occur outside of the original target element. Also, note that the moveHandler( ) and upHandler( ) functions that are registered to handle these events are defined as functions nested within beginDrag( ). Because they are defined in this nested scope, they can use the arguments and local variables of beginDrag( ), which considerably simplifies their implementation. Example 19-2. Dragging with the DOM Level 2 event model/** * Drag.js: * This function is designed to be called from a mousedown event handler. * It registers temporary capturing event handlers for the mousemove and * mouseup events that will follow and uses these handlers to "drag" the * specified document element. The first argument must be an absolutely * positioned document element. It may be the element that received the * mousedown event or it may be some containing element. The second * argument must be the event object for the mousedown event. **/ function beginDrag(elementToDrag, event) { // Figure out where the element currently is // The element must have left and top CSS properties in a style attribute // Also, we assume they are set using pixel units var x = parseInt(elementToDrag.style.left); var y = parseInt(elementToDrag.style.top); // Compute the distance between that point and the mouse-click // The nested moveHandler function below needs these values var deltaX = event.clientX - x; var deltaY = event.clientY - y; // Register the event handlers that will respond to the mousemove // and mouseup events that follow this mousedown event. Note that // these are registered as capturing event handlers on the document. // These event handlers remain active while the mouse button remains // pressed and are removed when the button is released. document.addEventListener("mousemove", moveHandler, true); document.addEventListener("mouseup", upHandler, true); // We've handled this event. Don't let anybody else see it. event.stopPropagation( ); event.preventDefault( ); /** * This is the handler that captures mousemove events when an element * is being dragged. It is responsible for moving the element. **/ function moveHandler(event) { // Move the element to the current mouse position, adjusted as // necessary by the offset of the initial mouse-click elementToDrag.style.left = (event.clientX - deltaX) + "px"; elementToDrag.style.top = (event.clientY - deltaY) + "px"; // And don't let anyone else see this event event.stopPropagation( ); } /** * This is the handler that captures the final mouseup event that * occurs at the end of a drag **/ function upHandler(event) { // Unregister the capturing event handlers document.removeEventListener("mouseup", upHandler, true); document.removeEventListener("mousemove", moveHandler, true); // And don't let the event propagate any further event.stopPropagation( ); } } You can use beginDrag( ) in an HTML file like the following (which is a simplified version of Example 19-2 with the addition of dragging): <script src="Drag.js"></script> <!-- Include the Drag.js script --> <!-- Define the element to be dragged --> <div style="position:absolute; left:100px; top:100px; background-color: white; border: solid black;"> <!-- Define the "handle" to drag it with. Note the onmousedown attribute. --> <div style="background-color: gray; border-bottom: dotted black; padding: 3px; font-family: sans-serif; font-weight: bold;" onmousedown="beginDrag(this.parentNode, event);"> Drag Me <!-- The content of the "titlebar" --> </div> <!-- Content of the dragable element --> <p>This is a test. Testing, testing, testing.<p>This is a test.<p>Test. </div> The key here is the onmousedown attribute of the inner <div> element. Although beginDrag( ) uses the DOM Level 2 event model, we register it here using the Level 0 model for convenience. As we'll discuss in the next section, the event models can be mixed, and when an event handler is specified as an HTML attribute, the event object is available using the event keyword. (This is not part of the DOM standard but is a convention of the Netscape 4 and IE event models, which are described later.) Here's another simple example of using beginDrag( ); it defines an image that the user can drag, but only if the Shift key is held down: <script src="Drag.js"></script> <img src="plus.gif" width="20" height="20" style="position:absolute; left:0px; top:0px;" onmousedown="if (event.shiftKey) beginDrag(this, event);"> Note the differences between the onmousedown attribute here and the one in the previous example. 19.2.8. Mixing Event ModelsSo far, we've discussed the traditional Level 0 event model and the new standard DOM Level 2 model. For backward compatibility, browsers that support the Level 2 model will continue to support the Level 0 event model. This means that you can mix event models within a document, as we did in the HTML fragments used to demonstrate the element-dragging script in the previous section. It is important to understand that web browsers that support the Level 2 event model always pass an event object to event handlers -- even handlers registered by setting an HTML attribute or a JavaScript property using the Level 0 model. When an event handler is defined as an HTML attribute, it is implicitly converted to a function that has an argument named event. This means that such an event handler can use the identifier event to refer to the event object. The DOM standard never formalized the Level 0 event model. It does not even require properties like onclick for HTML elements that support an onclick attribute. However, the standard recognizes that the Level 0 event model will remain in use and specifies that implementations that support the Level 0 model treat handlers registered with that model as if they were registered using addEventListener( ). That is, if you assign a function f to the onclick property of a document element e (or set the corresponding HTML onclick attribute), it is equivalent to registering that function as follows: e.addEventListener("click", f, false); When f( ) is invoked, it is passed an event object as its argument, even though it was registered using the Level 0 model. Furthermore, if you change the value of the onclick property from function f to function g, it is equivalent to this code: e.removeEventListener("click", f, false); e.addEventListener("click", g, false); Note, however, that the Level 2 specification does not say whether an event handler registered by assigning to the onclick property can be removed by calling removeEventListener( ). At the time of this writing, the Mozilla/Netscape implementation does not allow this. 19.2.9. Synthesizing EventsThe DOM Level 2 standard includes an API for creating and dispatching synthetic events. This API allows events to be generated under program control rather than under user control. Although this is not a commonly needed feature, it can be useful, for example, to produce regression tests that subject a DHTML application to a known sequence of events and verify that the result is the same. It could also be used to implement a macro playback facility to automate commonly performed user-interface actions. On the other hand, the synthetic event API is not suitable for producing self-running demo programs: you can create a synthetic mousemove event and deliver it to the appropriate event handlers in your application, but this does not actually cause the mouse pointer to move across the screen! Unfortunately, at the time of this writing, the synthetic event API is not supported by Netscape 6, nor by the current version of Mozilla. To generate a synthetic event, you must complete three steps:
To create an event object, call the createEvent( ) method of the Document object. This method takes a single argument, which is the name of the event module for which an event object should be created. For example, to create an event object suitable for use with a click event, call the method as follows: document.createEvent("HTMLEvents"); To create an event object suitable for use with any of the mouse event types, call it like this instead: document.createEvent("MouseEvents"); Note that the argument to createEvent( ) is plural. This is counterintuitive, but it is the same string that you'd pass to the hasFeature( ) method to test whether a browser supports an event module. After creating an event object, the next step is to initialize its properties. The properties of event objects are always read-only, however, so you cannot directly assign values to them. Instead, you must call a method to perform the initialization. Although the earlier descriptions of the Event, MouseEvent, and other event objects mentioned only properties, each object also defines a single method for initializing the properties of the event. This initialization method has a name that depends on the type of event object to be initialized and is passed as many arguments as there are properties to be set. Note that you can call an event initialization method only before dispatching a synthetic event: you cannot use these methods to modify the properties of an event object that is passed to an event handler. Let's look at a couple of examples. As you know, click events are part of the HTMLEvents module and use event objects of type Event. These objects are initialized with an initEvent( ) method, as follows: e.initEvent("click", "true", "true"); On the other hand, mousedown events are part of the MouseEvents module and use event objects of the MouseEvent type. These objects are initialized with an initMouseEvent( ) method that takes many more arguments: e.initMouseEvent("mousedown", true, false, // Event properties window, 1, // UIEvent properties 0, 0, 0, 0, // MouseEvent properties false, false, false, false, 0, null); Note that you pass only the event module name to createEvent( ). The name of the actual event type is passed to the event initialization method. The DOM standard does not require that you use one of the predefined names. You may create events using any event type name you choose, as long as it does not begin with a digit or with the prefix "DOM" (in uppercase, lowercase, or mixed case). If you initialize a synthetic event with a custom event type name, you must register event handlers with that event type name as well. After creating and initializing an event object, you can dispatch it by passing it to the dispatchEvent( ) method of the appropriate document element. dispatchEvent( ) is defined by the EventTarget interface, so it is available as a method of any document node that supports the addEventListener( ) and removeEventListener( ) methods. The element to which you dispatch an event becomes the event target, and the event object goes through the usual sequence of event propagation. At each stage of event propagation, the event object you created is passed to any event handlers that were registered for the event type you specified when you initialized the event. Finally, when event propagation finishes, your call to dispatchEvent( ) returns. The return value is false if any of the event handlers called the preventDefault( ) method on your event object and is true otherwise. Copyright © 2003 O'Reilly & Associates. All rights reserved. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|