Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning JavaScript With DOM Scripting And Ajax - From Novice To Professional (2006)

.pdf
Скачиваний:
80
Добавлен:
17.08.2013
Размер:
17.27 Mб
Скачать

166

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

Instead of addEventListener(), MSIE has attachEvent(); instead of passing an event object to each listener, MSIE keeps one global event object in window.event.

A developer named Scott Andrew came up with a portable function called addEvent() that works around the differences when it comes to adding events:

function addEvent(elm, evType, fn, useCapture) {

//Cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko

//By Scott Andrew

if (elm.addEventListener) { elm.addEventListener(evType, fn, useCapture); return true;

}else if (elm.attachEvent) {

var r = elm.attachEvent('on' + evType, fn); return r;

}else {

elm['on' + evType] = fn;

}

}

The function uses one more parameter than addEventListener(), which is the element itself. It tests whether addEventListener() is supported and simply returns true when it is able to attach the event in the W3C-compliant way.

Otherwise, it checks whether attachEvent() is supported (effectively meaning MSIE is used) and tries to attach the event that way. Notice that attachEvent() does need the “on” prefix for the event. For browsers that support neither addEventListener() nor attachEvent(), like MSIE on Mac, the function points the DOM-1 property to the function. This effectively overwrites any other events attached to this element in MSIE on Mac, but at least it works.

Note There is an ongoing discussion about how addEvent() can be improved—for example, to support retaining the option to send the current element as a parameter via this—and many clever solutions have been developed so far. As each has different drawbacks, I won’t go into details here, but if you are interested, check the comments at the addEvent() recoding contest page at quirksmode.org (http://www.quirksmode.org/blog/archives/2005/10/_and_the_winner_1.html).

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

167

As MSIE uses a global event, you cannot rely on the event object being sent to your listeners, but you need to write a different function to get the element that was activated. Matters get confused even further as the properties of window.event are slightly different from the ones of the W3C event object:

In Internet Explorer, target is replaced by srcElement.

button returns different values. In the W3C model, 0 is the left button, 1 is the middle, and 2 is the right; however, MSIE returns 1 for the left button, 2 for the right, and 4 for the middle. It also returns 3 when the left and right buttons are pressed together, and 7 for all three buttons together.

To accommodate these changes, you can use this function:

function getTarget(e){ var target; if(window.event){

target = window.event.srcElement;

}else if (e){

target = e.target;

}else {

target = null ;

}

return target;

}

Or more briefly, using the ternary operator:

getTarget:function(e){

var target = window.event ? window.event.srcElement : e ? e.target : null;

if (!target){return false;} return target;

}

168

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

Safari has a nasty bug (or feature—one is never sure): if you click a link, it does not send the link, but the text node contained in the link, as the target. A workaround is to check whether the element’s node name is really a link:

getTarget:function(e){

var target = window.event ? window.event.srcElement : e ? e.target : null; if (!target){return false;}

if (target.nodeName.toLowerCase() != 'a'){target = target.parentNode;} return target;

}

Preventing default actions and event bubbling also needs to accommodate the different browser implementations.

stopPropagation() is not a method in MSIE, but a property of the window event called cancelBubble.

preventDefault() is not a method either, but a property called returnValue.

This means that you have to write your own stopBubble() and stopDefault() methods:

stopBubble:function(e){

if(window.event && window.event.cancelBubble){ window.event.cancelBubble = true;

}

if (e && e.stopPropagation){ e.stopPropagation();

}

}

Note Safari supports stopPropagation() but does nothing with it. This means it will not stop the event from bubbling, and there is no quick way around that. Hopefully this will be fixed in future versions.

stopDefault:function(e){

if(window.event && window.event.returnValue){ window.event.cancelBubble = true;

}

if (e && e.preventDefault){ e.preventDefault();

}

}

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

169

As you normally want to stop both of these things from happening, it might make sense to collect them into one function:

cancelClick:function(e){

if (window.event && window.event.cancelBubble && window.event.returnValue){

window.event.cancelBubble = true; window.event.returnValue = false; return;

}

if (e && e.stopPropagation && e.preventDefault){ e.stopPropagation();

e.preventDefault();

}

}

Using these helper methods should allow you to handle events unobtrusively and across browsers, with one exception:

Safari understands the preventDefault() method but doesn’t implement what it should do. Therefore, if you add a handler to a link and call cancelClick() in the listener method, the link will still be followed.

The way around this for Safari is to add another dummy function via the old onevent syntax that stops the link from being followed. You’ll see this fix in action now. Let’s take the collapsing headline example again and replace the DOM-2–compliant methods and properties with the cross-browser helpers:

xBrowserListItemCollapse.js used in exampleXBrowserListItemCollapse.html

newshl = {

// CSS classes

overClass:'over', // Rollover effect hideClass:'hide', // Hide things currentClass:'current', // Open item

init:function(){ var ps,i,hl;

if(!document.getElementById || !document.createTextNode){return;} var newsList = document.getElementById('news'); if(!newsList){return;}

var newsItems = newsList.getElementsByTagName('li'); for(i = 0;i<newsItems.length;i++){

hl = newsItems[i].getElementsByTagName('a')[0]; DOMhelp.addEvent(hl,'click',newshl.toggleNews,false); hl.onclick = DOMhelp.safariClickFix;

170

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

DOMhelp.addEvent(hl,'mouseover',newshl.hover,false);

DOMhelp.addEvent(hl,'mouseout',newshl.hover,false);

}

var ps = newsList.getElementsByTagName('p'); for(i = 0;i<ps.length;i++){

DOMhelp.cssjs('add',ps[i],newshl.hideClass);

}

},

toggleNews:function(e){

var section = DOMhelp.getTarget(e).parentNode.parentNode; var first = section.getElementsByTagName('p')[0];

var action = DOMhelp.cssjs('check',first,newshl.hideClass)?'remove':'add'; var sectionAction = action == 'remove'?'add':'remove';

var ps = section.getElementsByTagName('p'); for(var i = 0;i<ps.length;i++){

DOMhelp.cssjs(action,ps[i],newshl.hideClass);

}

DOMhelp.cssjs(sectionAction,section,newshl.currentClass);

DOMhelp.cancelClick(e);

},

hover:function(e){

var hl = DOMhelp.getTarget(e).parentNode.parentNode; var action = e.type == 'mouseout'?'remove':'add'; DOMhelp.cssjs(action,hl,newshl.overClass);

}

}

DOMhelp.addEvent(window,'load',newshl.init,false);

Note hl.onclick = DOMhelp.safariClickFix; could be a simple hl.onclick = function(){return false;}; however, it will be easier to search and replace this fix once the Safari development team has sorted the problem out.

The clickable headlines now work across all modern browsers; however, it seems that you could streamline the script a bit. Right now the examples loop a lot, which is not really

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

171

necessary. Instead of hiding all paragraphs inside the list items individually, it is a lot easier to simply add a class to the list item and let the CSS engine hide all the paragraphs:

listItemCollapseShorter.css (excerpt)—used in exampleListItemCollapseShorter.html

#news li.hide p{ display:none;

}

#news li.current p{ display:block;

}

This way you can get rid of the inner loop through all the paragraphs in the init() method and replace it with one line of code that applies the hide class to the list item itself as follows:

listItemCollapseShorter.js (excerpt)—used in exampleListItemCollapseShorter.html

newshl={

// CSS classes

overClass:'over', // Rollover effect hideClass:'hide', // Hide things currentClass:'current', // Open item

init:function(){ var hl;

if(!document.getElementById || !document.createTextNode){return;} var newsList=document.getElementById('news'); if(!newsList){return;}

var newsItems=newsList.getElementsByTagName('li'); for(var i=0;i<newsItems.length;i++){

hl=newsItems[i].getElementsByTagName('a')[0];

DOMhelp.addEvent(hl,'click',newshl.toggleNews,false);

DOMhelp.addEvent(hl,'mouseover',newshl.hover,false);

DOMhelp.addEvent(hl,'mouseout',newshl.hover,false); hl.onclick = DOMhelp.safariClickFix;

DOMhelp.cssjs('add',newsItems[i],newshl.hideClass);

}

},

172

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

The next change is in the toggleNews() method. There you replace the loop with a simple if condition that checks whether the current class is applied to the list item and replaces hide with current if that is the case and current with hide if it isn’t. This shows or hides all the paragraphs inside the list item.

listItemCollapseShorter.js (excerpt)—used in exampleListItemCollapseShorter.html

toggleNews:function(e){

var section=DOMhelp.getTarget(e).parentNode.parentNode; if(DOMhelp.cssjs('check',section,newshl.currentClass)){ DOMhelp.cssjs('swap',section,newshl.currentClass,

newshl.hideClass);

}else{

DOMhelp.cssjs('swap',section,newshl.hideClass,

newshl.currentClass);

}

DOMhelp.cancelClick(e);

},

The rest stays the same:

listItemCollapseShorter.js (excerpt)—used in exampleListItemCollapseShorter.html

hover:function(e){

var hl = DOMhelp.getTarget(e).parentNode.parentNode; var action = e.type == 'mouseout'?'remove':'add'; DOMhelp.cssjs(action,hl,newshl.overClass);

}

}

DOMhelp.addEvent(window,'load',newshl.init,false);

Never Stop Optimizing

Analyzing your own code to determine what can be optimized in this way should never cease, even though it is very tempting in the heat of the moment to merrily code away and create something too complex for its own good.

Taking a step back, analyzing the problem you want to solve, and reevaluating what’s already in place is sometimes a lot more beneficial than just plowing on. In this case, the optimization is in leaving the hiding of the elements to the cascade in CSS, rather than looping through the child elements and hiding them individually.

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

173

When you take another look at code you have created, the following ideas are always good to keep in the back of your head:

Any idea that avoids nested loops is a good one.

Properties of the main object are a good place to store information that is of interest for several methods—for example, which element is active in site navigation.

If you find yourself repeating bits of code over and over again, create a new method that fulfills this task—if you have to change the code in the future, you’ll only have to change it in one place.

Don’t traverse the node tree too much. If a lot of elements need to know about some other element, find it once and store it in a property. This will shorten the code a lot, as something like contentSection is a lot shorter than elm.parentNode.parentNode.nextSibling.

A long list of if and else statements might be much easier handled as a switch/case block.

If something is likely to change in the future, like the Safari stopPropagation() hack, then it is a good idea to put it in its own method. The next time you see the code and you spot this seemingly useless method, you’ll remember what was going on.

Don’t rely on HTML too much. It is always the first thing to change (especially when there is a CMS involved).

The Ugly Page Load Problem and Its Ugly Solutions

When developers started to use CSS extensively, they soon encountered some annoying browser bugs. One of them was the flash of unstyled content, otherwise known as FOUC. (http://www.bluerobot.com/web/css/fouc.asp). This effect shows the page without a style sheet for a brief moment before applying it.

We now face the same problem with JavaScript-enhanced pages. If you load the example of the collapsed news items, you’ll see all the news expanded for a brief moment. This brief moment is the time needed for the document and all its dependencies like images and thirdparty content to finish loading.

This has been annoying scripting enthusiasts with a designer’s eye for a long time; the onload event fired when the page and all contained media like images was loaded, and that was it—until a lot of clever DOM scripters put their heads together and had a go at it.

Dean Edwards (a name to remember when it comes to highly technical but ingenious solutions) came up with a script that allows you to execute code before the page has finished loading all the content elements, thus allowing for a smoother looking interface without things jumping around. You can test his solution at http://dean.edwards.name/weblog/2005/09/ busted/.

174

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

The problem with this solution is that it relies heavily on browser-specific code and does not work on Safari or Opera. This might change in the future, so be sure to visit Dean’s site from time to time.

A different solution that is supported by all JavaScript-capable browsers, but is a bit dirty in terms of separation of structure, presentation, and behavior, is to write out the necessary CSS to hide the elements via document.write() in the document’s HEAD. If you wanted to apply this to the headline example, all you’d need to do is add the styles for the paragraphs inside the list item in the HTML document’s head.

exampleListItemCollapseNoFlash.html (excerpt)

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Example: Collapsing List Items without Flashing</title> <style type="text/css">

@import 'listItemCollapseNoFlash.css'; </style>

<script type="text/javascript" src="DOMhelp.js"></script>

<script type="text/javascript" src="listItemCollapseNoFlash.js"></script>

<script type="text/javascript"> document.write('<'+'style type="text/css">'); document.write('#news li p{display:none;}'); document.write('<'+'/style>');

</script>

</head>

Note The concatenation of the style tags is necessary to avoid validation programs complaining about invalid HTML.

Both versions are feasible but seem dirty, and so far, I don’t see any really clean solution for this problem.

Reading and Filtering Keyboard Entries

Probably the most common event for the web you’ll use is click, as it has the benefit of being supported by every element and can be triggered by both keyboard and mouse if the element in question can be reached via keyboard.

C H A P T E R 5 P R E S E N T A T I O N A N D B E H A V I O R ( C S S A N D E V E N T H A N D L I N G )

175

However, there is nothing stopping you from checking keyboard entries in JavaScript with the keyup or keypress handler. The former is the W3C standard; the latter is not in the standards and occurs after keydown and keyup, but it is well supported in browsers.

As an example of how you could read out and use keyboard entries, let’s write a script that checks whether the entered data in a form field is purely numbers. You’ve tested and converted entries to numbers in Chapter 2 already, but this time you want to check the entry while it occurs rather than after the user submits the form. If the user enters a nonnumerical character, the script should disable the Submit button and show an error message.

You start with a simple HTML form that has one entry field:

exampleKeyChecking.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<html dir="ltr" lang="en"> <head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Example: Checking keyboard entry</title>

<style type="text/css"> @import 'keyChecking.css';

</style>

<script type="text/javascript" src="DOMhelp.js"></script> <script type="text/javascript" src="keyChecking.js"></script>

</head>

<body>

<p class="ex">Keychecking example, try to enter anything but numbers in the form field below.</p>

<h1>Get Chris Heilmann's book cheaper!</h1> <form action="nothere.php" method="post"> <p>

<label for="Voucher">Voucher Number</label> <input type="text" name="Voucher" id="Voucher" /> <input type="submit" value="redeem" />

</p>

</form>

</body>

</html>

And after applying the following script, the browser will check what the user enters and both show an error message and disable the Submit button when the entry is not a number, as shown in Figure 5-7.