Chapter 7. Handling Events

7.0. Introduction

Events, especially within an interactive environment such as a browser, are what make JavaScript essential. There is very little JavaScript functionality that isn’t triggered by some event, even if the only event that occurs is a web page being loaded. Event handling in JavaScript depends on determining which event or events you want to trigger some activity, and then attaching JavaScript functionality to the event.

The earliest form of event handling is still one of the most common: the use of an event handler. An event handler is an element property that you can assign a function, such as the following, which assigns a function to the window.onload event handler:

window.onload=someFunction;

You can also assign an event handler directly in an element, such as in the opening body tag:

<body onload="someFunction()">

However, I don’t recommend embedding events directly in page elements, as it makes it difficult to find and change the event handling routines later. If you use the first approach, which is to assign a function to an event handler in script element or in a JavaScript library, you only have to look in one place in order to make changes.

Note

JavaScript lives in an enormous number of environments, as Chapter 21 demonstrates. Most of this chapter is focused on JavaScript within a browser or browser-like environment.

Some Common Events

There are several events that can be captured via JavaScript, though not all are available to all web page elements. The load and unload events are typically used with the window object to signal when the page is finished loading, and just before the page is unloaded because the web page reader is navigating away from the page. The submit and reset events are used with forms to signal when the form is submitted, or has been reset by the reader.

The blur and focus events are frequently used with form elements, to determine when an element gets focus, and loses it. Changing a form value can trigger the change event. The blur and change events are especially handy if you need to validate the form values.

Most web page elements can receive the click or dblclick event. Other mouse events include mousedown, mousemove, mouseout, mouseover, and mouseup, which can be used to track the cursor and mouse activity.

You can also track keyboard activity with events such as keydown and keyup, as well as keypress. If something can be scrolled, you can typically capture a scroll event.

Event History and New Event Handling

There’s a history to event handling with JavaScript. The earliest form of event handling is frequently termed “DOM Level 0” event handling, even though there is no DOM Level 0. It involves assigning a function directly to an event handler.

You can assign the event handler in the element directly:

<div onclick="clickFunction()">

or assign a function to the event handler in a script element or JavaScript library:

window.onload=someFunction;

In the last several years, a newer DOM Level 2 event handling system has emerged and achieved widespread use. With DOM Level 2 event handling, you don’t assign a function to an event handler directly; instead, you add the function as an event listener:

window.addEventListener("load",loadFunction,false);

The general syntax is:

targetElement.addEventListener(typeOfEvent,listenerFunction,
                 useCapture);

The last parameter in the function call has to do with how events are handled in a stack of nested elements. For example, if you’re capturing an event for a link within a div element, and you want both elements to do some processing based on the event, when you assign the event listener to the link, set the last parameter to false, so the event bubbles up to the div element.

You can also remove event listeners, as well as cancel the events themselves. You can also prevent the event from propagating if the element receiving the event is nested in another element. Canceling an event is helpful when dealing with form validation, and preventing event propagation is helpful when processing click events.

It’s up to you to decide which level of event handling meets your needs. If you’re doing simple event handling—not using any external JavaScript libraries and not worried about canceling events, or whether events bubble up in a stack of elements—you can use DOM Level 0 events. Most of the examples in this book use window.onload to trigger the demonstration JavaScript.

If you need the more sophisticated event handling, including the ability to more easily control events, you’ll want to use DOM Level 2 events. They can be easily encapsulated into a JavaScript library, and can be safely integrated into a multi-JavaScript library environment. However, the fly in this little event handling pie is that the DOM Level 2 event handling isn’t supported universally by all browsers: Microsoft has not implemented DOM Level 2 event handling with IE8 or earlier. However, it’s fairly simple to work around the cross-browser issues, as detailed in Recipe 7.3.

Note

There’s ongoing work to create a new DOM Level 3 event handling, which builds on the work of the DOM Level 2 event handling and is included as part of the Web Applications work at the W3C. However, implementation of the newer material is sparse, at best.

New Events, New Uses

There are newer events to go with the newer models, and to go with a nonbrowser-specific DOM. As examples of DOM events, the DOMNodeInserted and DOMNodeRemoved events are triggered when a node is added or removed from the page’s document tree. However, I don’t recommend using the W3C event for general web pages, as these events are not supported in the current versions of IE, and only partially supported in most other browsers. Most web application authors wouldn’t need these events, anyway.

There are also events associated with the increasingly popular mobile and other hand-held computing environments. For instance, Firefox has a nonstandard set of events having to do with touch swiping, which Mozilla calls the mouse gesture events. It’s interesting, but use with caution until there’s wider acceptance of the newer events. We’ll take a look at one type of mobile device event handling towards the end of the chapter.

See Also

See Recipe 7.3 for a demonstration of handling cross-browser event handling.

7.1. Detecting When the Page Has Finished Loading

Problem

You want to run a function after the page is finished loading.

Solution

Capture the load event via the onload event handler on the window:

window.onload=functionName;

Or:

window.onload=function() {
   var someDiv = document.getElementById("somediv");
   ...
}

Discussion

Prior to accessing any page element, you have to first make sure it’s been loaded into the browser. You could add a script block into the web page after the element. A better approach, though, is to capture the window load event and do your element manipulation at that time.

This recipe uses the DOM Level 0 event handling, which assigns a function or functionality to an event handler. An event handler is an object property with this syntax:

element.onevent=functionName;

Where element is the target element for the event, and onevent is the specific event handler. Since the event handler is a property of the window object, as other objects also have access to their own event handler properties, it’s accessed using dot notation (.), which is how object properties are accessed in JavaScript.

The window onload event handler in the solution is assigned as follows:

window.onload=functionName;

You could also use the onload event handler directly in the element. For the window load event, attach the onload event handler to the body element:

<body onload="functionName()">

Use caution with assigning event handlers directly in elements, though, as it becomes more difficult to find them if you need to change them at a later time. There’s also a good likelihood of element event handlers eventually being deprecated.

See Also

See Recipe 7.3 for a discussion about the problems with using this type of DOM Level 0 event handling.

7.2. Capturing the Location of a Mouse Click Event Using the Event Object

Problem

You need to discover the location in the web page where a mouse click occurred.

Solution

Assign the onclick event handler to the document object, and when the event handler function is processed, access the click location from the Event object:

document.onclick=processClick;
...
function processClick(evt) {

   // access event object
   evt = evt || window.event;
   var x = 0; var y = 0;

   // if event object has pageX property
   // get position using pageX, pageY
   if (evt.pageX) {
      x = evt.pageX;
      y = evt.pageY;

    // else if event has clientX property
   } else if (evt.clientX) {
      var offsetX = 0; offsetY = 0;

       // if documentElement.scrollLeft supported
      if (document.documentElement.scrollLeft) {
          offsetX = document.documentElement.scrollLeft;
          offsetY = document.documentElement.scrollTop;
      } else if (document.body) {
          offsetX = document.body.scrollLeft;
          offsetY = document.body.scrollTop;
      }

      x = evt.clientX + offsetX;
      y = evt.clientY + offsetY;
    }

alert ("you clicked at x=" + x + " y=" + y);
}

Discussion

Whoa! We didn’t expect all of this for a simple task such as finding the location in the page of a mouse click. Unfortunately, this recipe is a good demonstration of the cross-browser challenges associated with JavaScript event handling.

From the top: first, we need to find information about the event from the event object. The event object contains information specific to the event, such as what element received the event, the given location relative to the event, the key pressed for a keypress event, and so on.

In the solution, we immediately run into a cross-browser difference in how we access this object. In IE8 and earlier, the Event object is accessible from the Window object; in other browsers, such as Firefox, Opera, Chrome, and Safari, the Event object is passed, by default, as a parameter to the event handler function.

The way to handle most cross-browser object differences is to use a conditional OR (||) operator, which tests to see if the object is not null. In the case of the event object, the function argument is tested. If it is null, then window.event is assigned to the variable. If it isn’t, then it’s reassigned to itself:

evt = evt || window.event;

You’ll also see another approach that’s not uncommon, and uses the ternary operator, to test to see if the argument has been defined and is not null. If it isn’t, the argument is assigned back to the argument variable. If it is, the window.event object is accessed and assigned to the same argument variable:

evt = evt ? evt : window.event;

Once we’ve worked through the event object difference, we’re on to the next. Firefox, Opera, Chrome, and Safari both get the mouse location in the web page via the nonstandard event pageX and pageY properties. However, IE8 doesn’t support these properties. Instead, your code can access the clientX and clientY properties, but it can’t use them as is. You have to adjust the value to account for any offset value, due to the window being scrolled.

Again, to find this offset, you have to account for differences, primarily because of different versions of IE accessing your site. Now, we could consider disregarding anything older than IE6, and that’s an option. For the moment, though, I’ll show support for versions of IE both older and newer than IE6.

For IE6 strict and up, you’ll use document.documentElement.scrollLeft and document.documentElement.scrollTop. For older versions, you’ll use document.body.scrollLeft and document.body.scrollTop. Here’s an example of using conditional statements:

var offsetX = 0; offsetY = 0;
if (document.documentElement.scrollLeft) {
   offsetX = document.documentElement.scrollLeft;
   offsetY = document.documentElement.scrollTop;
} else if (document.body) {
   offsetX = document.body.scrollLeft;
   offsetY = document.body.scrollTop;
}

Once you have the horizontal and vertical offsets, add them to the clientX and clientY values, respectively. You’ll then have the same value as what you had with pageX and pageY:

x = evt.clientX + offsetX;
y = evt.clientY + offsetY;

To see how all of this holds together, Example 7-1 shows a web page with a red box. When you click anywhere in the web page, the box is moved so that the top-left corner of the box is positioned exactly where you clicked. The example implements all of the various cross-browser workarounds, and operates safely in all the main browser types, even various older browsers.

Example 7-1. Capturing the mouse-click location and moving element to location
<!DOCTYPE html>
<head>
<title>Box Click Box Move</title>
<style type="text/css">

#info
{
  width: 100px; height: 100px;
  background-color: #ff0000;
  position: absolute;
  top: 0;
  left: 0;
}
</style>
<script>

window.onload=function() {
  document.onclick=processClick;
}

function processClick(evt) {
  evt = evt || window.event;
  var x = 0; var y = 0;
  if (evt.pageX) {
    x = evt.pageX;
    y = evt.pageY;
  } else if (evt.clientX) {
    var offsetX = 0; offsetY = 0;
    if (document.documentElement.scrollLeft) {
       offsetX = document.documentElement.scrollLeft;
       offsetY = document.documentElement.scrollTop;
    } else if (document.body) {
       offsetX = document.body.scrollLeft;
       offsetY = document.body.scrollTop;
    }

    x = evt.clientX + offsetX;
    y = evt.clientY + offsetY;
  }

  var style = "left: " + x + "px; top: " + y + "px";
  var box = document.getElementById("info");
  box.setAttribute("style", style);
}
</script>
</head>
<body>
<div id="info"></div>
</body>

Cross-browser differences may seem overwhelming at times, but most of the code can be packaged into a library as a reusable event handler. However, there is a good likelihood that many of these browser differences will vanish with IE9.

Note

The example doesn’t work with IE7 because of the use of setAttribute with the style attribute. The downloaded example contains a workaround.

See Also

Mozilla has, as usual, excellent documentation on the event object at https://developer.mozilla.org/En/DOM/Event. See Recipe 12.15 for a description of setAttribute.

7.3. Creating a Generic, Reusable Event Handler Function

Problem

You want to implement DOM Level 2 event handling, but the solution needs to be reusable and cross-browser friendly.

Solution

Create a reusable event handler function that implements DOM Level 2 event handling, but is also cross-browser friendly. Test for object support to determine which functions to use:

function listenEvent(eventTarget, eventType, eventHandler) {
   if (eventTarget.addEventListener) {
      eventTarget.addEventListener(eventType, eventHandler,false);
   } else if (eventTarget.attachEvent) {
      eventType = "on" + eventType;
      eventTarget.attachEvent(eventType, eventHandler);
   } else {
      eventTarget["on" + eventType] = eventHandler;
   }
}
...
listenEvent(document, "click", processClick);

Discussion

The reusable event handler function takes three arguments: the target object, the event (as a string), and the function name. The object is first tested to see if it supports addEventListener, the W3C DOM Level 2 event listener method. If it is supported, this method is used to map the event to the event handler function.

The first two arguments to addEventListener are the event string and the event handler function. The last argument in addEventListener is a Boolean indicating how the event is handled with nested elements (in those cases where more than one element has an event listener attached to the same event). An example would be a div element in a web page document, where both the div and document have event listeners attached to their click events.

In the solution, the third parameter is set to false, which means that the listener for an outer element (document) doesn’t “capture” the event, but allows it to be dispatched to the nested elements (the div) first. When the div element is clicked, the event handler function is processed for the div first, and then the document. In other words, the event is allowed to “bubble up” through the nested elements.

However, if you set the third parameter to true, the event is captured in the outer element, and then allowed to “cascade down” to the nested elements. If you were to click the div element, the document’s click event would be processed first, and then the div.

Most of the time, we want the events to bubble up from inner nested elements to outer elements, which is why the generic function is set to false by default. However, to provide flexibility, you could add a third parameter to the reusable function, allowing the users to determine whether they want the default bubble-up behavior or the cascade-down instead.

Note

Most JavaScript libraries, applications, and developers assume that events are processed in a bubble-up fashion. If you are using external material, be very cautious about setting events to be cascade-down.

To return to the solution, if addEventListener is not supported, then the application checks to see if attachEvent is supported, which is what Microsoft supports in IE8. If this method is supported, the event is first modified by prepending “on” to it (required for use with attachEvent), and then the event and event handler function are mapped.

Notice that attachEvent doesn’t have a third parameter to control whether the event is bubble-up or cascade-down. That’s because Microsoft only supports bubble-up.

Lastly, in the rare case where neither addEventListener nor attachEvent is supported, the fallback method is used: the DOM Level 0 event handling. If we’re not worried about very old browsers, such as IE5, we can leave off the last part.

Why go through all of this work when we can just use the simpler DOM Level 0 event handling? The quick answer is compatibility.

When we use DOM Level 0 event handling, we’re assigning an event handler function to an object’s event:

document.onclick=functionName;

If we’re using one or more JavaScript libraries in addition to our code, we can easily wipe out the JavaScript library’s event handling. The only way to work around the overwriting problem is the following, created by highly respected developer Simon Willison:

function addLoadEvent(func) {
  var oldonload = window.onload;
  if (typeof window.onload != 'function') {
    window.onload = func;
  } else {
    window.onload = function() {
      if (oldonload) {
        oldonload();
      }
      func();
    }
  }
}

In the function, whatever the window.onload event handler is assigned, is assigned to a variable. The type is checked and if it’s not a function, we can safely assign our function. If it is, though, we assign an anonymous function to the event handler, and in the anonymous function, invoke both our function and the one previously assigned.

However, a better option is to use the more modern event listeners. With DOM Level 2 event listeners, each library’s function is added to the event handler without overwriting what already exists; they’re even processed in the order they’re added.

Note

The differences in how browsers handle events goes much deeper than what method to use. Recipes 7.5 and 7.7 cover other differences associated with the cross-browser event handling.

To summarize, if you’re just doing a quick and dirty JavaScript application, without using any libraries, you can get away with DOM Level 0 events. I use them for most of the small examples in this book. However, if you’re building web pages that could use libraries some day, you’re better off using DOM Level 2. Luckily, most JavaScript libraries already provide the cross-browser event handling functions.

Reusable, DOM Level 2 event handling: piece of cake. Except that we’re not finished.

Creating a universal stop-listening function

There will be times when we want to stop listening to an event, so we need to create a cross-browser function to handle stopping events:

function stopListening (eventTarget,eventType,eventHandler) {
   if (eventTarget.removeEventListener) {
      eventTarget.removeEventListener(eventType,eventHandler,false);
   } else if (eventTarget.detachEvent) {
      eventType = "on" + eventType;
      eventTarget.detachEvent(eventType,eventHandler);
   } else {
      eventTarget["on" + eventType] = null;
   }
}

Again, from the top: the W3C DOM Level 2 method is removeEventListener. If this method exists on the object, it’s used; otherwise, the object is tested to see if it has a method named detachEvent, Microsoft’s version of the method. If found, it’s used. If not, the final DOM Level 0 event is to assign the event handler property to null.

Now, once we want to stop listening to an event, we can just call stopListening, passing in the same three arguments: target object, event, and event handler function.

Note

IE8 does not support DOM Level 2 event handling, but IE9 will. However, until older versions of IE, such as IE7 and IE8, vanish from use, you’ll most likely need to continue using the cross-browser technique covered in this recipe.

7.4. Canceling an Event Based on Changed Circumstance

Problem

You need to cancel an event, such as a form submission, before the event propagates to other elements.

Solution

Create a reusable function that will cancel an event:

// cancel event
function  cancelEvent (event) {
   if (event.preventDefault) {
      event.preventDefault();
   } else {
      event.returnValue = false;
   }
}

...

function validateForm(evt) {
   evt = evt || window.event;
   cancelEvent(evt);
}

Discussion

You may want to cancel an event before it completes processing, such as preventing the default submission of a form, if the form elements don’t validate.

If you were using a purely DOM Level 0 event handling procedure, you could cancel an event just by returning false from the event handler:

function formSubmitFunction() {
...
if (bad)
   return false
}

However, with the new, more sophisticated event handling, we need a function like that shown in the solution, which cancels the event, regardless of event model.

In the solution, the event is passed as an argument to the function from the event handler function. If the event object has a method named preventDefault, it’s called. The preventDefault method prevents the default action, such as a form submission, from taking place.

If preventDefault is not supported, the event property returnValue is set to false in the solution, which is equivalent to what we did with returning false from the function for DOM Level 0 event handling.

7.5. Preventing an Event from Propagating Through a Set of Nested Elements

Problem

You have one element nested in another. Both capture the click event. You want to prevent the click event from the inner element from bubbling up or propagating to the outer event.

Solution

Stop the event from propagating with a generic routine that can be used with the element and any event:

// stop event propagation
function  cancelPropagation (event) {
   if (event.stopPropagation) {
       event.stopPropagation();
   } else {
      event.cancelBubble = true;
   }
}

In the event handler for the inner element click event, call the function, passing in the event object:

cancelPropagation(event);

Discussion

If we don’t want to cancel an event, but do want to prevent it from propagating, we need to stop the event propagation process from occurring. For now, we have to use a cross-browser method, since IE8 doesn’t currently support DOM Level 2 event handling.

In the solution, the event is tested to see if it has a method called stopPropagation. If it does, this event is called. If not, then the event cancelBubble property is set to true. The first method works with Chrome, Safari, Firefox, and Opera, while the latter property works with IE.

To see this work, Example 7-2 shows a web page with two div elements, one nested in another and both assigned a click event handler function. When the inner div element is clicked, two messages pop up: one for the inner div element, one for the other. When a button in the page is pressed, propagation is turned off for the event. When you click the inner div again, only the message for the inner div element is displayed.

Example 7-2. Preventing an event from propagating among nested elements
<!DOCTYPE html>
<head>
<title>Prevent Propagation</title>
<style>
#one
{
   width: 100px; height: 100px; background-color: #0f0;
}
#two {
   width: 50px; height: 50px; background-color: #f00;
}
#stop
{
  display: block;
}
</style>
<script>

// global for signaling propagation cancel
var stopPropagation = false;

function listenEvent(eventTarget, eventType, eventHandler) {
   if (eventTarget.addEventListener) {
      eventTarget.addEventListener(eventType, eventHandler,false);
   } else if (eventTarget.attachEvent) {
      eventType = "on" + eventType;
      eventTarget.attachEvent(eventType, eventHandler);
   } else {
      eventTarget["on" + eventType] = eventHandler;
   }
}

// cancel propagation
function  cancelPropagation (event) {
   if (event.stopPropagation) {
      event.stopPropagation();
   } else {
      event.cancelBubble = true;
   }
}

listenEvent(window,"load",function() {
   listenEvent(document.getElementById("one"),"click",clickBoxOne);
   listenEvent(document.getElementById("two"),"click",clickBoxTwo);
   listenEvent(document.getElementById("stop"),"click",stopProp);
  });

function stopProp() {
    stopPropagation = true;
}

function clickBoxOne(evt) {
  alert("Hello from One");
}

function clickBoxTwo(evt) {
  alert("Hi from Two");
  if (stopPropagation) {
     cancelPropagation(evt);
  }
}
</script>

</head>
<body>
<div id="one">
<div id="two">
<p>Inner</p>
</div>
</div>
<button id="stop">Stop Propagation</button>
</body>

The button event handler only sets a global variable because we’re not worried about its event propagation. Instead, we need to call cancelPropagation in the click event handler for the div elements, when we’ll have access to the actual event we want to modify.

Example 7-2 also demonstrates one of the challenges associated with cross-browser event handling. There are two click event handler functions: one for the inner div element, one for the outer. Here’s a more efficient click handler method function:

function clickBox(evt) {
   evt = evt || window.event;
   alert("Hi from " + this.id);
   if (stopPropagation) {
      cancelPropagation(evt);
   }
}

This function combines the functionality contained in the two functions in the example. If stopPropagation is set to false, both elements receive the event. To personify the message, the identifier is accessed from the element context, via this.

Unfortunately, this won’t work with IE8. The reason is that event handling with attachEvent is managed via the window object, rather than the DOM. The element context, this, is not available. You can access the element that receives the event via the event object’s srcElement property. However, even this doesn’t work in the example, because the srcElement property is set to the first element that receives the event, and isn’t updated when the event is processed for the next element as the event propagates through the nested elements.

When using DOM Level 0 event handling, these problems don’t occur. Microsoft has access to the element context, this, in the handler function, regardless of propagation. We could use the above function only if we assign the event handler for the two div elements using DOM Level 0:

document.getElementById("one").onclick=clickBox;
document.getElementById("two").onclick=clickBox;

7.6. Capturing Keyboard Activity

Problem

You want to capture keyboard events for a textarea, and look for specific character uses.

Solution

Capture the keyboard activity for a textarea element and check the ASCII value of the character to find the character or characters of interest:

var inputTextArea = document.getElementById("source");
listenEvent(inputTextArea,"keypress",processKeyStroke);
function processKeyStroke(evt) {
   evt = evt ? evt : window.event;
   var key = evt.charCode ? evt.charCode : evt.keyCode;
   // check to see if character is ASCII 38, or the ampersand (&)
   if (key == "38")
      ...

Discussion

There are multiple events associated with the keyboard:

keydown

Key is pressed down.

keyup

Key is released.

keypress

Follows keydown.

textInput

Follows keypress (Safari only).

Let’s look more closely at keypress and keydown.

When a key is pressed down, it generates a keydown event, which records that a key was depressed. The keypress event, though, represents the character being typed. If you press the Shift key and the “L” key to get a capital letter L, you’ll get two events if you listen to keydown, but you only get one event if you listen with keypress. The only time you’ll want to use keydown is if you want to capture every key stroke.

In the solution, the keypress event is captured and assigned an event handler function. In the event handler function, the event object is accessed to find the ASCII value of the key pressed (every key on a keyboard has a related ASCII numeric code). Cross-browser functionality is used to access this value: IE and Opera do not support charCode, but do support keyCode; Safari, Firefox, and Chrome support charCode.

Note

Not listed in the possible keyboard events is the textInput event, which is part of the new DOM Level 3 event specification currently in draft state. It also represents a character being typed. However, its implementation is sparse, and based on an editor draft, not a released specification. Stick with keypress for now.

To demonstrate how keypress works, Example 7-3 shows a web page with a textarea. When you open the page, you’ll be prompted for a “bad” ASCII character code, such as 38 for an ampersand (&). When you start typing in the textarea, all of the characters are reflected in the textarea except for the “bad” character. When this character is typed, the event is canceled, the default keypress behavior is interrupted, and the character doesn’t appear in the textarea.

Example 7-3. Preventing a keyboard value based on ASCII value of key
<!DOCTYPE html>
<head>
<title>Filtering Input</title>
<script>

var badChar;

function listenEvent(eventTarget, eventType, eventHandler) {
   if (eventTarget.addEventListener) {
      eventTarget.addEventListener(eventType, eventHandler,false);
   } else if (eventTarget.attachEvent) {
      eventType = "on" + eventType;
      eventTarget.attachEvent(eventType, eventHandler);
  } else {
     eventTarget["on" + eventType] = eventHandler;
  }
}

// cancel event
function  cancelEvent (event) {
   if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
   } else {
      event.returnValue = false;
      event.cancelBubble = true;
   }
}

window.onload=function() {
   badChar = prompt("Enter the ASCII value of the keyboard
key you want to filter","");
   var inputTA = document.getElementById("source");
   listenEvent(inputTA,"keypress",processClick);
}
function processClick(evt) {
   evt = evt || window.event;
   var key = evt.charCode ? evt.charCode : evt.keyCode;

   // zap that bad boy
   if (key == badChar) cancelEvent(evt);
}

</script>
</head>
<body>
<form>
<textarea id="source" rows="20" cols="50"></textarea>
</form>
</body>

Other than being a devious and rather nasty April Fool’s joke you can play on your blog commenters, what’s the purpose of this type of application?

Well, a variation of the application can be used to filter out nonescaped characters such as ampersands in comments for pages served up as XHTML. XHTML does not allow for nonescaped ampersands. The program could also be adapted to listen for ampersands, and replace them with the escaped versions (&amp;). Normally, you would escape text as a block on the server before storing in the database, but if you’re doing a live echo as a preview, you’ll want to escape that material before you redisplay it to the web page.

And it makes a wicked April Fool’s joke on your blog commenters.

Note

The use of the prompt can trigger a security warning with IE.

See Also

See Recipes 7.3 and 7.4 for an explanation of the event handler functions shown in Example 7-2.

7.7. Using the New HTML5 Drag-and-Drop

Problem

You want to incorporate the use of drag-and-drop into your web page, and allow your users to move elements from one place in the page to another.

Solution

Use the new HTML5 native drag-and-drop, which is an adaptation of the drag-and-drop technique supported in Microsoft Internet Explorer. Example 7-4 provides a demonstration.

Example 7-4. Using the new HTML5 drag-and-drop
<!DOCTYPE html>
<head>
<title>HTML5 Drag-and-Drop</title>
<style>
#drop
{
  width: 300px;
  height: 200px;
  background-color: #ff0000;
  padding: 5px;
  border: 2px solid #000000;
}
#item
{
    width: 100px;
    height: 100px;
    background-color: #ffff00;
    padding: 5px;
    margin: 20px;
    border: 1px dashed #000000;
}
*[draggable=true] {
  -moz-user-select:none;
  -khtml-user-drag: element;
  cursor: move;
}

*:-khtml-drag {
  background-color: rgba(238,238,238, 0.5);
}

</style>
<script>

function listenEvent(eventTarget, eventType, eventHandler) {
   if (eventTarget.addEventListener) {
      eventTarget.addEventListener(eventType, eventHandler,false);
   } else if (eventTarget.attachEvent) {
      eventType = "on" + eventType;
      eventTarget.attachEvent(eventType, eventHandler);
  } else {
     eventTarget["on" + eventType] = eventHandler;
  }
}

// cancel event
function  cancelEvent (event) {
   if (event.preventDefault) {
      event.preventDefault();
   } else {
      event.returnValue = false;
   }
}

// cancel propagation
function  cancelPropagation (event) {
   if (event.stopPropagation) {
      event.stopPropagation();
   } else {
      event.cancelBubble = true;
   }
}

window.onload=function() {
   var target = document.getElementById("drop");
   listenEvent(target,"dragenter",cancelEvent);
   listenEvent(target,"dragover", dragOver);
   listenEvent(target,"drop",function (evt) {
              cancelPropagation(evt);
              evt = evt || window.event;
              evt.dataTransfer.dropEffect = 'copy';
              var id = evt.dataTransfer.getData("Text");
              target.appendChild(document.getElementById(id));
              });

   var item = document.getElementById("item");
   item.setAttribute("draggable", "true");
   listenEvent(item,"dragstart", function(evt) {
               evt = evt || window.event;
               evt.dataTransfer.effectAllowed = 'copy';
               evt.dataTransfer.setData("Text",item.id);
               });

};

function dragOver(evt) {
  if (evt.preventDefault) evt.preventDefault();
  evt = evt || window.event;
  evt.dataTransfer.dropEffect = 'copy';
  return false;
}
</script>
</head>
<body>
<div>
<p>Drag the small yellow box with the dash border to the larger
red box with the solid border</p>
</div>
<div id="item" draggable="true">
</div>
<div id="drop">
</div>
</body>

Discussion

As part of the HTML5 specification, drag-and-drop has been implemented natively, though Opera doesn’t currently support drag-and-drop and it can be a bit tricky in Firefox, Chrome, Safari, and IE8. The example does not work with IE7, either.

Note

Currently, implementations of HTML5 drag-and-drop are not robust or consistent. Use with caution until the functionality has broader support.

Drag-and-drop in HTML5 is not the same as in previous implementations, because what you can drag is mutable. You can drag text, images, document nodes, files, and a host of objects.

To demonstrate HTML5 drag-and-drop at its simplest, we’ll explore how Example 7-4 works. First, to make an element draggable, you have to set an attribute, draggable, to true. In the example, the draggable element is given an identifier of item. Safari also requires an additional CSS style setting:

-khtml-user-drag: element;

The allowable values for -khtml-user-drag are:

element

Allows element to be dragged.

auto

Default logic to determine whether element is dragged (only images, links, and text can be dragged by default).

none

Element cannot be dragged.

Since I’m allowing a div element to be dragged, and it isn’t a link, image, or text, I needed to set -khtml-user-drag to element.

The element that can be dragged must have the draggable attribute set to true. It could be set in code, using setAttribute:

item.setAttribute("draggable","true");

However, IE doesn’t pick up the change in state; at least, it doesn’t pick up the CSS style change for a draggable object. Instead, it’s set directly on the object:

<div id="item" draggable="true">
</div>

The dragstart event handler for the same element is where we set the data being transferred with the drag operation. In the example, since I’m dragging an element node, I’m going to set the data type to “text” and provide the identifier of the element (accessible via target, which has the element context). You can specify the element directly, but this is a more complex operation. For instance, in Firefox, I could try the following, which is derived from the Mozilla documentation:

evt.dataTransfer.setData("application/x-moz-node",target);

and then try to process the element at the drop end:

var item = evt.dataTransfer.getData("application/x-moz-node");
target.appendChild(item);

However, the item gets set to a serialized form of the div element, not an actual reference to the div element. In the end, it’s easier just to set the data transfer to text, and transfer the identifier for the element, as shown in Example 7-4.

Note

You can also set several different kinds of data to be transferred, while your drop targets only look for specific types. There doesn’t have to be a one-to-one mapping. Also note that WebKit/Safari doesn’t handle the MIME types correctly unless you use getData and setData specifically.

The next part of the drag-and-drop application has to do with the drag receiver. In the example, the drag receiver is another div element with an identifier of “drop”. Here’s another instance where things get just a tad twisted.

There are a small group of events associated with HTML5 drag-and-drop:

dragstart

Drag event starts.

drag

During the drag operation.

dragenter

Drag is over the target; used to determine if target will accept drop.

dragover

Drag is over target; used to determine feedback to user.

drop

Drop occurs.

dragleave

Drag leaves target.

dragend

Drag operation ends.

The dragged item responds to dragstart, drag, and dragend. The target responds to dragenter, dragover, dragleave, and drop.

When the dragged object is over the target, if the target wants to signal it can receive the drop, it must cancel the event. Since we’re dealing with browsers that implement DOM Level 2 event handling, the event is canceled using the cancelEvent reusable function created in an earlier recipe. The code also returns false:

function dragOver(evt) {
  if (evt.preventDefault) evt.preventDefault();
  evt = evt || window.event;
  evt.dataTransfer.dropEffect = 'copy';
  return false;
}

In addition, the dragenter event must also be canceled, either using preventDefault or returning false in the inline event handler, as shown in the example.

Note

Both dragenter and dragover must be canceled if you want the target element to receive the drop event.

When the drop event occurs, the drop event handler function uses getData to get the identifier for the element being dragged, and then uses the DOM appendChild method to append the element to the new target. For a before and after look, Figure 7-1 shows the page when it’s loaded and Figure 7-2 shows the page after the drag-and-drop event.

Web page with drag-and-drop enabled; before drag-and-drop
Figure 7-1. Web page with drag-and-drop enabled; before drag-and-drop
Web page after drag-and-drop operation
Figure 7-2. Web page after drag-and-drop operation

I also had to cancel event propagation with the receiver element, though it’s not required in all browsers.

In the example, I use an anonymous function for the dragstart and drop event handler functions. The reason is that, as mentioned in Recipe 7.5, the attachEvent method supported by Microsoft does not preserve element context. By using an anonymous function, we can access the element in the other function scope, within the event handler functions.

I haven’t been a big fan of drag-and-drop in the past. The operation requires a rather complex hand-and-mouse operation, which limits the number of people who can take advantage of this interactive style. Even if you have no problems with fine motor skills, using drag-and-drop with a touch screen or mouse pad can be cumbersome. It’s also been extremely problematic to implement.

The current HTML5 specification details newly standardized drag-and-drop functionality, though not everyone is pleased by the capability. Peter-Paul Koch (PPK) of the well-known Quirksblog describes the current implementation of drag-and-drop in HTML5 as the “HTML5 drag-and-drop disaster”, and be aware that the article may not be safe for work). Why? According to PPK, there are several things wrong with the implementation: too many events, confusing implementation details, and inconsistencies.

One of the biggest concerns PPK mentions is the requirement that if an element is to receive a drop, when it receives either the dragenter or dragover event, it has to cancel the events.

I sympathize, but I understand some of the decisions that went into the current implementation of drag-and-drop in HTML5. For instance, we’re dealing with a legacy event handling model, so we can’t invent new event triggers just for drag-and-drop. If, by default, an element is not a valid drag-and-drop target, then the drag-and-drop operation must continue as another target is sought. If the dragged element enters or is over a valid target, though, by canceling the dragenter and dragover events, the target is signaling that yes, it is interested in being a target, and doing so using the only means available to it: by canceling the dragenter and dragover events.

PPK recommends using what he calls “old school” implementations of drag-and-drop. The only issue with those is they aren’t cross-browser friendly, and can be complicated to implement. They’re also not accessible, though accessibility can be added.

Gez Lemon wrote an article for Opera on using the WAI-ARIA (Web Accessibility Initiative–Accessible Rich Internet Applications) accessible drag-and-drop features. It’s titled “Accessible drag and drop using WAI-ARIA”. He details the challenges associated with making accessible drag-and-drop, as well as the two drag-and-drop properties associated with WAI-ARIA:

aria-grabbed

Set to true when element is selected for dragging.

aria-dropeffect

Effect that happens when source is released on drag target.

The aria-dropeffect has the following values:

copy

Source is duplicated and dropped on target.

move

Source is removed and moved to target.

reference

A reference or shortcut to source is created in target.

execute

Function is invoked, and source is input.

popup

Pop-up menu or dialog presented so user can select an option.

none

Target will not accept source.

In the article, Gez provides a working example using the existing drag-and-drop capability, and also describes how ARIA can be integrated into the new HTML5 drag-and-drop. The example provided in the article currently only works with Opera, which hasn’t implemented HTML5 drag-and-drop. Yet. But you can check out Figure 7-3 to see how drag-and-drop would look from a keyboard perspective.

What a keyboard-enabled drag-and-drop operation could look like
Figure 7-3. What a keyboard-enabled drag-and-drop operation could look like

Though old school drag-and-drop is complicated, several libraries, such as jQuery, provide this functionality, so you don’t have to code it yourself. Best of all, ARIA accessibility is being built into these libraries. Until the HTML5 version of drag-and-drop is widely supported, you should make use of one of the libraries.

In addition, as the code demonstrates, drag-and-drop can follow the old school or the new HTML5 method, and the libraries can test to see which is supported and provide the appropriate functionality. We can use the jQuery script.aculo.us, or Dojo drag-and-drop, now and in the future, and not worry about which implementation is used.

See Also

Safari’s documentation on HTML5 drag-and-drop can be found at http://developer.apple.com/mac/library/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/DragAndDrop.html. Mozilla’s documentation is at https://developer.mozilla.org/en/Drag_and_Drop. The HTML5 specification section related to drag-and-drop can be accessed directly at http://dev.w3.org/html5/spec/Overview.html#dnd.

See Recipe 7.5 for more on the complications associated with advanced cross-browser event listening, and Recipes 7.3, 7.4, and 7.5 for descriptions of the advanced event listening functions. Recipe 12.15 covers setAttribute.

7.8. Using Safari Orientation Events and Other Mobile Development Environments

Problem

You want to determine if the Safari browser accessing your web page has changed its orientation from portrait to landscape.

Solution

Use the proprietary orientation events for Safari:

window.onorientationchange = function () {
   var orientation = window.orientation;
   switch(orientation) {
      case 0:
            // portrait orientation
            break;
      case 90:
      case -90:
            // landscape orientation
            break;
      }
}

Discussion

We’re not going to spend a great deal of time on mobile devices—in this chapter, or in the rest of the book. The primary reason is that most mobile devices either react to your web page as any other browser would react, albeit in miniature, or the requirements for mobile development are so extensive as to warrant a separate book.

In addition, many small device and touch screen events are proprietary to the browser, such as the one demonstrated in the solution for this recipe.

In the solution, we want to capture an orientation change, since devices like the iPhone can flip from portrait to landscape. In the solution, we use the iPhoneOrientation events to first capture the orientation change, and then determine the new orientation.

Based on orientation, we can do things such as swap a portrait thumbnail for a landscape-based image, or even activate some form of motion. When Apple rolled out the iPhoneOrientation example, it provided a snow globe that would “shake” when orientation changed.

Typically, though, sophisticated mobile applications, such as many of those for the iPhone/iPod touch, are not web-based. They use a SDK provided by the company, which may actually require that you purchase a subscription. As for general web access for mobile devices, you’re better off providing mobile CSS files for mobile devices. Use proportional sizes for any pop ups and code your page regardless of device. In fact, many mobile devices turn off JavaScript for web pages automatically. As long as you ensure your web page is based on progressive enhancement (fully functional without script), you should be set for the mobile world.

See Also

Apple’s iPhoneOrientation script can be found at http://developer.apple.com/safari/library/samplecode/iPhoneOrientation/index.html. XUI is a JavaScript framework for mobile devices currently under development, and is accessible at http://xuijs.com/. jQTouch is a jQuery plug-in for mobile devices, at http://www.jqtouch.com/. I also recommend Jonathan Stark’s book, Building iPhone Apps with HTML, CSS, and JavaScript (O’Reilly).

Get JavaScript Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.