Browser sniffing mainly comes in two flavors; user agent sniffing and object detection.. Listing 10.1 Browser sniffing to fix event listening function addEventHandlerelement, type, listener {
Trang 1Listing 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 which panel 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];
Trang 2while (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;
Trang 3padding: 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 tabbedPanelobject 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
Trang 4Listing 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 surroundstay-ings, 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
Trang 510
Feature Detection
Aspiring 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
Trang 610.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
EventListenerinterface 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)) {
Trang 7element.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 piece of 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 the user agent string became increasingly hard due to 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);
} }
Trang 8This example suffers many of the same problems as that of our user agent sniffer
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 detection still exists in the wild Many of the popular libraries
still to this day use browser detection, and even user agent sniffing, to solve certain
cross-browser challenges Do a search for userAgent or browser 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 ideniden-tify 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
Trang 910.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 addEventHandleris 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 () {
Trang 10return 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 will usually 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