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.
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
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
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.
<!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.
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.
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.
<!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
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:
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
.
<!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 (&
).
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.
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.
<!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:
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.
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:
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.
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.