ptg 9.5 Unobtrusive Tabbed Panel Example 193 Listing 9.10 Using the tab controller (function () { if (typeof document == "undefined" || !document.getElementById) { return; } var dom = tddjs.dom; var ol = document.getElementById("news-tabs"); /* */ try { var controller = tddjs.ui.tabController.create(ol); dom.addClassName(ol.parentNode, "js-tabs"); controller.onTabChange = function (curr, prev) { dom.removeClassName(getPanel(prev), "active-panel"); dom.addClassName(getPanel(curr), "active-panel"); }; controller.activateTab(ol.getElementsByTagName("a")[0]); } catch (e) {} }()); The getPanel function used in the above example uses the semantic markup to find whichpanel an anchor should toggle. It extracts the part of the anchor’s href attribute after the hash character, looks up elements with corresponding names, and finally picks the first one it finds. It then traverses the element’s parent until it finds a div element. The method can be seen in Listing 9.11. Listing 9.11 Finding the panel to toggle (function () { /* */ function getPanel(element) { if (!element || typeof element.href != "string") { return null; } var target = element.href.replace(/.*#/, ""); var panel = document.getElementsByName(target)[0]; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 194 Unobtrusive JavaScript while (panel && panel.tagName.toLowerCase() != "div") { panel = panel.parentNode; } return panel; } /* */ }()); Note that getPanel defensively checks its argument and aborts if it doesn’t receive an actual element. This means that we can fearlessly call it using the curr and prev anchors in the onTabChange method, even though the prev argument will be undefined the first time it is called. To make the tabbed panels appear as panels, we can sprinkle on some very simple CSS, as seen in Listing 9.12. Listing 9.12 Simple tabbed panel CSS .js-tabs .section { clear: left; display: none; } .js-tabs .active-panel { display: block; } .js-tabs .nav { border-bottom: 1px solid #bbb; margin: 0 0 6px; overflow: visible; padding: 0; } .js-tabs .nav li { display: inline; list-style: none; } .js-tabs .nav a { background: #eee; border: 1px solid #bbb; line-height: 1.6; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 9.5 Unobtrusive Tabbed Panel Example 195 padding: 3px 8px; } .js-tabs a.active-tab { background: #fff; border-bottom-color: #fff; color: #000; text-decoration: none; } All the style rules are prefixed with “.js-tabs”, which means that they will only take effect if the script in Listing 9.10 completes successfully. Thus, we have a nice tabbed panel in browsers that support it and fall back to inline bookmarks and vertically presented panels of text in unsupporting browsers. Implementation of the unobtrusive tabs might strike you as a bit verbose and it is not perfect. It is, however, a good start—something to build on. For instance, rather than coding the panel handling inline as we just did, we could create a tabbedPanel object to handle everything. Its create method could receive the outer div element as argument and set up a tabController and offer something like the getPanel function as a method. It could also improve the current solution in many ways, for example, by checking that the tabs do not activate panels outside the root element. By implementing the tabController separately, it can easily be used for similar, yet different cases. One such example could be building a tabbed panel widget in which the links referenced external URLs. The onTabChange callback could in this case be used to fetch the external pages using XMLHttpRequest.By design, this tabbed panel would fall back to a simple list of links just like the panel we just built. Because the original unobtrusive example used the jQuery library, we could of course have done so here as well. By using it where appropriate, we’d end up shaving off quite a few lines of code. However, although the script would end up shorter, it would come with an additional 23kB (minimum) of library code. The unobtrusive tab controller we just built weigh in at less than 2kB, have no external dependencies, and work in more browsers. As a final note, I want to show you a compact idiomatic jQuery solution as well. Listing 9.13 shows the tabbed panel implemented in about 20 lines of (heavily wrapped) code. Note that this solution does not check markup before enabling the panel, and cannot be reused for other similar problems in a meaningful way. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 196 Unobtrusive JavaScript Listing 9.13 Compact jQuery tabbed panels jQuery.fn.tabs = function () { jQuery(this). addClass("js-tabs"). find("> ol:first a"). live("click", function () { var a = jQuery(this); a.parents("ol").find("a").removeClass("active-tab"); a.addClass("active-tab"); jQuery("[name="+this.href.replace(/^.*#/, "") + "]"). parents("div"). addClass("active-panel"). siblings("div.section"). removeClass("active-panel"); }); return this; }; 9.6 Summary In this chapter we have discussed the principles of unobtrusive JavaScript and how they can help implement websites using progressive enhancement. A particularly obtrusive implementation of tabbed panels served to shed some light on the prob- lems caused by making too many assumptions when coding for the client. Unobtrusive JavaScript describes clean code the JavaScript way, including stay- ing clean in its interaction with its surroundings, which on the web must be assumed to be highly unstable and unpredictable. To show how unobtrusive code can be implemented to increase accessibility potential, lower error rates, and provide a more maintainable solution, we snuck a peek into a test-driven development session that culminated in an unobtrusive tabbed panel that works in browsers as old as Internet Explorer 5.0, uses no external library, and disables itself gracefully in unsupporting environments. In Chapter 10, Feature Detection, we will take the concept of making no as- sumptions even further, and formalize some of the tests we used in this chapter as we dive into feature detection, an important part of unobtrusive JavaScript. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 10 Feature Detection A spiring JavaScript developers developing for the general web are faced with a rather unique challenge, in that very little is known about the environments in which scripts will execute. Even though we can use web analytics to gather information about our visitors, and external resources such as Yahoo’s graded browser support to guide us in decisions relevant to cross-browser development, we cannot fully trust these numbers; neither can they help make our scripts future proof. Writing cross-browser JavaScript is challenging, and the number of available browsers is increasing. Old browsers see new version releases, the occasional new browser appears (the most recent noticeable one being Google Chrome), and new platforms are increasingly becoming a factor. The general web is a minefield, and our task is to avoid the mines. Surely we cannot guarantee that our scripts will run effortlessly on any unknown environment lurking around the Internet, but we should be doing our very best to avoid ruining our visitors’ experience based on bad assumptions. In this chapter we will dive into the technique known as feature detection, arguably the strongest approach to writing robust cross-browser scripts. We will see how and why browser detection fails, how feature detection can be used in its place, and how to use feature detection to allow scripts to adjust in response to collecting knowledge about the environment’s capabilities. 197 From the Library of WoweBook.Com Download from www.eBookTM.com ptg 198 Feature Detection 10.1 Browser Sniffing For as long as there has been more than one browser in popular use, developers have tried to differentiate between them to either turn down unsupported browsers, or provide individual code paths to deal with differences between them. Browser sniffing mainly comes in two flavors; user agent sniffing and object detection. 10.1.1 User Agent Sniffing Sniffing the user agent is a primitive way of detecting browsers. By inspecting the contents of the User-Agent HTTP header, accessible through navigator. userAgent, script authors have branched their scripts to run IE specific code for IE and Netscape-specific code for Netscape, or commonly, deny access to unsup- ported browsers. Unwilling to have their browsers discriminated against, browser vendors adjusted the User-Agent header sent by the browser to include strings known to allow the browser access. This is evident to this day; Internet Explorer still includes the word “Mozilla” in its user agent string and Opera stopped identifying itself as Internet Explorer not too long ago. As if browsers with built-in lies weren’t enough, most browsers today even allow their users to manually choose how the browser should identify itself. That’s about as unreliable identification as you can find. Event handling has traditionally been rocky terrain to cover consistently across browsers. The simple event properties we used in Chapter 9, Unobtrusive JavaScript, is supported by just about any browser in use today, whereas the more sophisticated EventListener interface from the level 2 DOM specification is not. The spec calls for any Node to implement this interface, which among other things define the addEventListener method. Using this method we can add numerous event listeners to an event for a specific element, and we needn’t worry about the event property accidentally being overwritten. Most browsers available today support the addEventListener method, unfortunately with the exception of Internet Explorer (including version 8). IE does, however, provide the attachEvent method, which is similar and can be used to emulate common use cases. A naive way to work around this could involve the use of user agent sniffing, as seen in Listing 10.1. Listing 10.1 Browser sniffing to fix event listening function addEventHandler(element, type, listener) { // Bad example, don't try this at home if (/MSIE/.test(navigator.userAgent)) { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 10.1 Browser Sniffing 199 element.attachEvent("on" + type, function () { // Pass event as argument to the listener and // correct it's this value. IE calls the listener // with the global object as this. return listener.call(element, window.event); }); } else { element.addEventListener(type, listener, false); } } This pieceof code makes many mistakes, but alas, is representative of lots of code in use even to this day. The user agent sniff is potentially dangerous in a couple of ways; it assumes that any browser that does not appear to be Internet Explorer sup- ports addEventListener; it assumes that any browser appearing to be Internet Explorer supports attachEvent, and makes no room for a future Internet Ex- plorer that supports the standardized API. In other words, the code will err on some browsers and definitely will need updating whenever Microsoft releases a standards- compliant browser. We will improve on the example throughout this chapter. 10.1.2 Object Detection As sniffing theuser agent string became increasingly hard dueto dishonest browsers, browser detection scripts grew more sophisticated. Rather than inspecting the user agent string, developers discovered that the type of browser could very often be determined by checking for the presence of certain objects. For instance, the script in Listing 10.2 updates our previous example to avoid the user agent string and rather infer type of browser based on some objects known to exist only in Internet Explorer. Listing 10.2 Using object detection to sniff browser function addEventHandler(element, type, listener) { // Bad example, don't try this at home if (window.ActiveXObject) { element.attachEvent("on" + type, function () { return listener.call(element, window.event); }); } else { element.addEventListener(type, listener, false); } } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 200 Feature Detection This example suffers manyof the same problems asthat of our user agentsniffer. Object detection is a very useful technique, but not to detect browsers. Although unlikely, there is no guarantee that browsers other than Internet Ex- plorer won’t provide a global ActiveXObject property. For instance, older ver- sions of Opera imitated several aspects of Internet Explorer, such as the propri- etary document.all object, to avoid being blocked by scripts that employed bad browser detection logic. The basic premise of browser detection relies on upfront knowledge about the environments that will run our scripts. Browser detection, in any form, does not scale, is not maintainable, and is inadequate as a cross-browser scripting strategy. 10.1.3 The State of Browser Sniffing Unfortunately,browser detectionstill existsin thewild. Manyof the popular libraries still to this day use browser detection, and even user agent sniffing, to solve certain cross-browser challenges. Do a searchfor userAgent orbrowser in your favorite JavaScript library, and more likely than not, you will find several decisions made based on which browser the script thinks it’s faced with. Browser sniffs cause problems even when they are used only to make certain exceptions for certain browsers, because they easily break when new browser ver- sions are released. Additionally, even if a sniff could be shown to positively iden- tify a certain browser, it cannot be easily shown to not accidentally identify other browsers that may not exhibit the same problems the sniffs were designed to smooth over. Because browser detection frequently requires updating when new browsers are released, libraries that depend on browser sniffs put a maintenance burden on you, the application developer. To make the situation even worse, these updates are not necessarily backwards compatible, and may require you to rewrite code as well. Using JavaScript libraries can help smooth over many difficult problems, but often come at a cost that should be carefully considered. 10.2 Using Object Detection for Good Object detection, although no good when used to detect browsers, is an excellent technique for detecting objects. Rather than branching on browser, a much sounder approach is branching on individual features. Before using a given feature, the script can determine whether it is available, and in cases in which the feature is known to have buggy implementations, the script can test the feature in a controlled setting to determine if it can be relied upon. This is the essence of feature detection. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 10.2 Using Object Detection for Good 201 10.2.1 Testing for Existence Consider once again our event handling example. Listing 10.3 uses object detection as before, but rather than testing objects known to only exist in certain browsers, it tests the objects we’re actually interested in using. Listing 10.3 Using feature detection to branch event handling function addEventHandler(element, type, listener) { if (element.addEventListener) { element.addEventListener(type, listener, false); } else if (element.attachEvent && listener.call) { element.attachEvent("on" + type, function () { return listener.call(element, window.event); }); } else { // Possibly fall back to event properties or abort } } This example has a much better chance of surviving in the wild, and is very un- likely to need updating whenever a new browser is released. Internet Explorer 9 is scheduled to implement addEventListener, and even if this browser keeps attachEvent side by side with it to ensure backwards compatibility, our addEventHandler is going to do the right thing. Prodding for features rather than browser type means our script will use addEventListener if it’s available without any manual interference. The preceding browser detection-based scripts will all have to be updated in such a scenario. 10.2.2 Type Checking Although Listing 10.3 prods the correct objects before using them, the feature test is not completely accurate. The fact that the addEventListener property exists is not necessarily a guarantee that it will work as expected. The test could be made more accurate by checking that it is callable, as Listing 10.4 shows. Listing 10.4 Type-checking features function addEventHandler(element, type, listener) { if (typeof element.addEventListener == "function") { element.addEventListener(type, listener, false); } else if (typeof element.attachEvent == "function" && typeof listener.call == "function") { element.attachEvent("on" + type, function () { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 202 Feature Detection return listener.call(element, window.event); }); } else { // Possibly fall back to DOM0 event properties or abort } } This example employs more specific feature tests, and should ideally produce fewer false positives. Unfortunately, it does not work at all in certain browsers. To understand why, we need to familiarize ourselves with native and host objects. 10.2.3 Native and Host Objects Any object whose semantics are described by the ECMAScript specification is known as a native object. By the nature of their definition, the behavior of native objects is generally predictable and, as such, using specific feature tests such as the type-check in Listing 10.4 willusually provide valuable information. However, given a buggy environment, we may encounter a browser whose typeof implementation is doing the wrong thing even if the object in question is in fact callable and works as expected. By making a feature test more specific we reduce the chances of false positives, but at the same time we demand more from the environment, possibly increasing the chances of false negatives. Objects provided by the environment but not described by the ECMAScript specification are known as host objects. For example, a browser’s DOM implemen- tation consists solely of host objects. Host objects are problematic to feature test because the ECMAScript specification defines them very loosely; “implementation- defined” is commonly found in the description of host object behavior. Host objects are, among other things, afforded the luxury of defining their own result for typeof. In fact, the third edition of the ECMAScript specification does not restrict this result in any way, and host objects may return “undefined” when used with typeof, should they so wish. Although attachEvent most definitely is callable in Internet Explorer, the browser is not cooperative in purveying this information when asked with typeof, as Listing 10.5 shows. Listing 10.5 typeof and host objects in Internet Explorer // true in Internet Explorer, including version 8 assertEquals("object", typeof document.attachEvent); As if this result wasn’t bad enough, other host objects such as ActiveX objects are even worse to work with. Listing 10.6 shows a few surprising results. From the Library of WoweBook.Com Download from www.eBookTM.com . CSS .js-tabs .section { clear: left; display: none; } .js-tabs .active-panel { display: block; } .js-tabs .nav { border-bottom: 1px solid #bbb; margin: 0 0 6px; overflow: visible; padding: 0; } .js-tabs. Example 195 padding: 3px 8px; } .js-tabs a.active-tab { background: #fff; border-bottom-color: #fff; color: #000; text-decoration: none; } All the style rules are prefixed with “.js-tabs”, which means that. visible; padding: 0; } .js-tabs .nav li { display: inline; list-style: none; } .js-tabs .nav a { background: #eee; border: 1px solid #bbb; line-height: 1.6; From the Library of WoweBook.Com Download