Chapter 8. Scripting CSS

Booming by a hilly golf course in a new pair of lime green Nike Lunar Elites, I observed some deer running through the deep snow that a blizzard referred to as "Snowmageddon" had dumped on Pittsburgh. Though I love to run in fresh powder, 3 feet could be too much. But the deer did just fine. Besides, in those lime green shoes, I felt invincible. So, off the road and into the snow I went.

Probably ten minutes later, my heart thumping forcefully enough to burst, I glanced down at my run timer. Ugh. Still 70 minutes left. I was going to drop dead before it beeps, I thought. So, after cresting the highest hilltop, raising my fists, and rasping "Drago!" a few times, I turned tail and made for the road to finish my run.

But midway through the downhill, the snow depth suddenly increased, sending me airborne less one lime green shoe. Landing in a snow drift, I flailed about for a while trying to right myself. Eventually I had to more or less swim to shallower snow. Finding my missing shoe, I then continued to the road without further mishap and finished my run. Passersby certainly gave me some odd looks, dusty with snow as I was from head to toe!

Now then, deer with their four spindly legs and hooves are better designed for deep-snow running than we are with our two bulkier legs and feet. In the same way, Firefox, Safari, and Opera are better designed to script CSS with DOM than Internet Explorer is. So in this chapter, we will have to dumb things down for Internet Explorer. That is to say, we'll put some snow shoes on our DOM dummy so that it can keep up!

DOM Interfaces for Working with CSS

In Chapter 7, we explored some DOM interfaces for querying markup. So as you might guess, in this chapter, we will explore those for querying CSS. We won't explore all of them, just some essential ones:

CSSStyleDeclaration
CSS2Properties
StyleSheet
CSSStyleSheet
CSSRuleList
CSSRule
CSSStyleRule
CSSImportRule
ElementCSSInlineStyle
ViewCSS
DocumentStyle
StyleSheetList

Remember from Chapter 7 that an interface is a feature list for an object. That is, an interface tells you what methods and members an object contains for your scripting pleasure. So, those are the names to look under on the Web or in a printed DOM reference when you inevitably forget what I tell you here!

Clarifying Some CSS Jargon

Before we get rolling, let's go over some CSS jargon. Consider the following:

img {display:block;border:0;}

The whole enchilada, img {display:block;border:0;}, is referred to as a rule. Rules have two parts. First there is a selector indicating what part of the document to which to apply the rule. In our sample rule, img is the selector. So, the rule is for any img element in the document. The second part of a rule is the declaration block. They contain one or more declarations. In our sample rule, {display:block;border:0;} is the declaration block. display:block; is a declaration. border:0; is too. A declaration pairs a property with a value. That means in our rule, the display property is paired with the block value. And the border property is paired with the 0 value.

How Does JavaScript Represent a Rule?

You can't query a CSS rule unless you know what to query. JavaScript represents a CSS rule like img {display:block;border:0;} with a CSSStyleRule object. They receive the following two members from CSSStyleRule:

selectorText
style

Then they receive four more from CSSRule:

cssText
parentRule
parentStyleSheet
type

That is, they do in Firefox, Safari, and Opera. Internet Explorer implements CSSStyleRule but not CSSRule. So, we will dumb things down for Internet Explorer and work with just selectorText and style.

Note

CSSStyleRule objects have the members listed in both the CSSStyleRule and CSSRule interfaces. So, there are two lists of features for this kind of DOM object.

Moving right along, here's a question for you: for our sample rule, img {display:block;border:0;}, what do you think CSSStyleRule.selectorText would contain?

Yup, "img". So, selectorText contains the rule's selector as a string. Who'd have thought?

The other member, CSSStyleRule.style, is not so simple. JavaScript represents a declaration block like {display:block;border:0;} with a CSSStyleDeclaration object, which is what the style member contains.

Now then, a CSSStyleDeclaration object has the following features listed in the CSSStyleDeclaration interface:

cssText
length
parentRule
getPropertyCSSValue()
getPropertyPriority()
getPropertyValue()
item()
removeProperty()
setProperty()

Now for the bad news—the Internet Explorer news. Other than cssText, Internet Explorer does not implement any methods or members from CSSStyleDeclaration. We will have to pretend cssText is the only CSSStyleDeclaration member. Even though this is not true for Firefox, Safari, and Opera, it is for dummy Internet Explorer. Sigh and move forward. It is what it is.

On the bright side, pretending CSSStyleDeclaration has one member means I can only give you a one-question test: for our sample declaration block, {display:block;border:0;}, what would CSSStyleDeclaration.cssText contain?

Here's a hint: some CSS text.

Hmm. cssText contains CSS text.

That much was obvious. Sorry.

OK, what do you think?

Close, but not quite. Though cssText does contain the CSS code of the declaration block, it doesn't contain the curly braces. For a CSS rule like {display:block;border:0;}, cssText would contain "display:block;border:0;", not {display:block;border:0;}.

Internet Explorer failing to implement any of the CSSStyleDeclaration methods for querying property values would seem to be a disaster. I mean if Internet Explorer is totally illiterate, which is to say it cannot read or write CSS properties, how do we script CSS cross-browser?

Don't panic.

To simplify querying the property-value pairings within a declaration block, JavaScript extends CSSStyleDeclaration with an optional interface named CSS2Properties that Internet Explorer, Firefox, Safari, and Opera all implement. CSS2Properties adds one member for every CSS property in the CSS2 standard to a CSSStyleDeclaration object. So for any CSS property, reading a CSS2Properties member is equivalent to invoking CSSStyleDeclaration.getPropertyValue(). Conversely, writing a CSS2Properties member is equivalent to invoking CSSStyleDeclaration.setProperty().

In other words, CSSStyleDeclaration.getProperty() and CSSStyleDeclaration.setProperty() are redundant, so it's no big deal that Internet Explorer fails to implement them.

Just as a reminder, insofar as CSS2Properties members are named with an identifier, you may query those with the . operator and an identifier or with the [] operator and a string, typically in the form of a variable or parameter. If you are foggy on how those operators work, flip back to Chapter 3. I'll wait for you to return. Promise.

One-word CSS properties such as display or left have identically named CSS2Properties members. But JavaScript identifiers cannot have dashes. But you know that from Chapter 1. So, any CSS property containing dashes has a CSS2Properties member that is named in camel case rather than in dash case. For example, margin-bottom is named marginBottom. Finally, float is a JavaScript reserved word. So, float is represented by a member named cssFloat in Firefox, Safari, and Opera. For whatever reason, Internet Explorer deviates from DOM and names its member styleFloat instead...just roll your eyes.

CSS2Properties members are named with camel case identifiers. What about their values? Those are always strings. Yup, always. Even if you want to set a property like left or border to 0, you have to assign "0".

Whenever we want to script declarations, remember those are property-value pairings, we will query either CSS2Properties members or the CSSStyleDeclaration member, cssText, since those are the only features that dummy Explorer knows about. So if we had a variable named myDeclarations containing a CSSStyleDeclaration object, it would contain an object equivalent to the following:

var myDeclarations = {
  display: "block",
  border: "0",
  cssText: "display:block;border:0;"
}

I did that one. Now it's your turn. Create an object literal equivalent to the CSSStyleRule representing our sample rule, img {display:block;border:0;}. Save it to a variable named myRule.

Here's a hint: one of the members might just contain my object literal.

One more minute.

Time is up.

Did you create an object literal like the following? Give yourself a pat on the back.

var myRule = {
  selectorText: "img",
  style: {
    display: "block",
    border: "0",
    cssText: "display:block;border:0;"
  }
}

Two Other Declaration Blobs

CSSStyleDeclaration and CSS2Properties provide a way for you to query the declarations in a rule. Additionally, those enable you to work with two other kinds of declaration blobs that we'll explore later in the day. First, the cumulative declarations from the CSS cascade that target an element (however, note that those declarations are read-only), and second, the declarations in an element's style attribute. These declarations are represented by ElementCSSInlineStyle.style and, like the declarations in a rule, are read-write. Therefore, you can change CSS property values in a rule or style attribute, but not the cumulative ones from the cascade.

Note

For any tag in your markup lacking a style attribute, the element representing the tag will still have a style member containing a CSSStyleDeclaration object, which may have some default members from the DTD.

Downloading the Sample Files

Download or code the following markup, eight.html, from this book's web site:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Scripting CSS</title>
<link rel="stylesheet" type="text/css" href="eight.css" id="spriteStyles" />
</head>
<body>
<div id="running">
  <h4>Running</h4>
  <ul class="blue">
    <li><a id="adidas" href="http://www.adidas.com">adidas</a></li>
    <li><a id="asics" href="http://www.asics.com">ASICS</a></li>
    <li><a id="brooks" href="http://www.brooksrunning.com">Brooks</a></li>
    <li><a id="newBalance" href="http://www.newbalance.com">New Balance</a></li>
    <li><a id="nike" href="http://www.nike.com">Nike</a></li>
    <li><a id="saucony" href="http://www.saucony.com">Saucony</a></li>
  </ul>
</div>
</body>
</html>

Additionally, download or code the following CSS file, eight.css. Put it in the same folder with eight.html.

* {
  margin:0;
  padding:0;
  border:0;
}
body {
  background:rgb(255,255,255);
  color:rgb(0,0,0);
  font:11px Verdana, Arial, Helvetica, sans-serif;
}
div#running {
  position:absolute;
  left:40px;
  top:40px;
  width:120px;
  height:243px;
  background:url(images/container.gif) 0 0 no-repeat;
}
div#running h4 {
  position:absolute;
  left:0px;
  top:0px;
  width:63px;
  height:25px;
  text-indent:−9999px;
  text-decoration:none;
  overflow:hidden;
}
div#running li {
  display:inline;
}
div#running li a {
  position:absolute;
  left:10px;
  width:100px;
  height:28px;
  color:rgb(0,0,0);
text-indent:−9999px;
  text-decoration:none;
  overflow:hidden;
}
ul.blue a {
  background-image:url(images/blue.gif);
}
ul.fuchsia a {
  background-image:url(images/fuchsia.gif);
}
ul.green a {
  background-image:url(images/green.gif);
}
a#adidas {
  top:30px;
  background-position:0 0;
}
a#asics {
  top:65px;
  background-position:0 −27px;
}
a#brooks {
  top:100px;
  background-position:0 −54px;
}
a#newBalance {
  top:135px;
  background-position:0 −81px;
}
a#nike {
  top:170px;
  background-position:0 −108px;
}
a#saucony {
  top:205px;
  background-position:0 −135px;
}

Finally, download the sprite images: blue.gif, fuchsia.gif, and green.gif. Download the background image, container.gif, too. Put those four images in a folder aptly named images within the folder you put eight.html and eight.css in. If you are unfamiliar with sprites, they work by sliding an image to different coordinates by way of the CSS property background-position. As Figure 8-1 illustrates, this enables us to combine all 12 link images into a single image, which improves load time.

The blue sprite contains all 12 button images.

Figure 8-1. The blue sprite contains all 12 button images.

Querying a Style Attribute

Open eight.html in Firefox, and then press F12 to enable Firebug. If you're just joining us, flip back to the preface for details on how to do this.

Now let's try querying the style attribute for the Nike link. Insofar as its markup tag does not contain a style attribute, ElementCSSInlineStyle.style will contain a bunch of "" empty strings. That makes this a pretty dull read. Nevertheless, take a peek at some CSS properties:

var myStyle = document.getElementById("nike").style;
myStyle.backgroundPosition;
// ""
myStyle.backgroundImage;
// ""
myStyle.left;
// ""
myStyle.top;
// ""

As Figure 8-2 displays, JavaScript returns one "" empty string after another. If you abide by the separation of markup content from CSS presentation credo, this will be the case for every Element node in the DOM tree—at least initially.

Just one ""empty string after another

Figure 8-2. Just one ""empty string after another

Let's see what I mean by that cryptic comment. Go ahead and write the members from the previous sample like so, and click Run, verifying your work with Figure 8-3:

var myStyle = document.getElementById("nike").style;
myStyle.backgroundPosition = "−99px −108px";
myStyle.backgroundImage = "url(images/fuchsia.gif)";
myStyle.left = "200px";
myStyle.top = "30px";
Writing some inline styles

Figure 8-3. Writing some inline styles

We replaced the blue sprite with the fuchsia one, slid the sprite to the left in order to reveal the down button, and moved the link entirely out of the container. It's pretty draconian, but it illustrates the power of writing an element's style member: scripted declarations in ElementCSSInlineStyle.style override those from anywhere else in the CSS cascade.

One more thing: take another peek at ElementCSSInlineStyle.style for the Nike link. There's something there other than one "" empty string after another now:

var myStyle = document.getElementById("nike").style;
myStyle.backgroundPosition;
// "−99px −108px"
myStyle.left;
// "200px"

So, our writing four CSS properties by way of ElementCSSInlineStyle.style was equivalent to doing so by way of the following markup:

<a style="background-position:−99px −108px;background-
image:url(images/fuchsia.gif);left:200px;top:30px" id="nike"
href="http://www.nike.com">Nike</a>

In turn, this is why our scripted styles override those from elsewhere in the cascade, in other words, from eight.css.

We're good with querying CSS2Properties members. Now let's take a peek at CSSStyleDeclaration.cssText. Clear both Firebug panels, but do not refresh Firefox. This leaves our scripted styles in place so cssText has something for us to read other than an "" empty string. Enter and run the following, verifying your work with Figure 8-4:

myStyle.cssText;
// "background-position: −99px −108px; background-image: url("images/fuchsia.gif"); left:
200px; top: 30px;"
Querying CSSStyleDeclaration.cssText

Figure 8-4. Querying CSSStyleDeclaration.cssText

cssText contains the CSS text of the style attribute. Imagine that. Now take a peek at cssText for the adidas link:

document.getElementById("adidas").style.cssText;
// ""

It's just a dull "" empty string. If you fully separate CSS from markup, reading cssText will be as mind-numbing as reading CSS2Properties members—unless you have a thing for "" empty strings. On the other hand, writing cssText provides a way to change several CSS properties in one fell swoop. Want to try doing that? Me too. Refresh Firefox, clear Firebug, and then enter and run the following sample. Note that the second statement wraps to two lines in this book but should be keyed in as one line in Firebug:

var myStyle = document.getElementById("brooks").style;
myStyle.cssText = "background-image:url(images/fuchsia.gif);left:210px;top:0;padding-
left:99px;height:55px";

That did in one assignment statement what would have taken five with CSS2Properties members. Now for the tricky part: writing cssText wipes away any previous inline style declarations. With this in mind, click Clear in Firebug, but do not refresh Firefox, and then enter the following assignment; however, before clicking Run, take a guess at what will happen. You get a Smiley Cookie if you get it right.

myStyle.cssText = "background-image:url(images/fuchsia.gif)";

Note

Smiley Cookies are a Pittsburgh institution. If you want to try them, visit www.eatnpark.com, and click the Create your own Smiley Cookies!link. Trust me, you'll love them.

Does your guess correspond to Figure 8-5?

Any declarations you write to cssText totally overwrite those already in there.

Figure 8-5. Any declarations you write to cssText totally overwrite those already in there.

Don't feel bad. I didn't think you'd get that one right. What's going on here? Any declarations you write to cssText totally overwrite those already in there. So, our declarations for left, top, padding-left, and height disappeared. This in turn means that the declarations for those properties in eight.css now shine through. Therefore, other than swapping the blue sprite for the fuchsia one, our brooks link is styled the same as when the page initially loaded. This is equivalent to the following markup:

<a style="background-image:url(images/fuchsia.gif);" id="brooks"
href="http://www.brooksrunning.com">Brooks</a>

Note that when you write the value of cssText, property names are dash case, not camel case—just like in the CSS standard.

Scripting Classes

Writing CSS2Properties members or cssText provides a way to restyle one element, but what if you want to do so for several elements? One way is to script className, typically of an ancestor element. Remember that for any element, the className member represents the class attribute.

In our included style sheet, eight.css, we have a rule for a blue class and a fuchsia class. The latter is dormant. That is to say, no element in our markup is a member of the fuchsia class. So, refresh Firefox, clear Firebug, and let's change the className member for the <ul> element from blue to fuchsia. Doing so, as Figure 8-6 illustrates, swaps the sprite for all six links in one fell swoop:

document.getElementsByTagName("ul")[0].className = "fuchsia";
Writing the className member to swap the blue sprite for the fuchsia one

Figure 8-6. Writing the className member to swap the blue sprite for the fuchsia one

Totally overwriting className like that is fine if the element is a member of just one class that you want to replace. But an element can be a member of two or more classes. So, is there a less draconian way to go about scripting className?

Of course. Why else would I even bring it up? Scripting className is something you will do quite often. With this in mind, let's write a function to do the job for us. Maybe name it swapClass. Then refresh Firefox, clear Firebug, and enter the following:

function swapClass(element, oldClass, newClass) {
  var re = new RegExp("\b" + oldClass + "\b", "g");
  element.className = element.className.replace(re, newClass);
}

swapClass() works with three parameters. element is an Element node that you want to change className for. newClass is the name of the class you want to replace oldClass with. Note that both oldClass and newClass are strings.

swapClass() does its work in just two statements. First, we save a regular expression to a local variable named re. This will match a word boundary, followed by the value of oldClass, followed by another word boundary. So, re will match "blue" but not "blueberry". In the next statement, we call String.replace() on className, passing re for the first parameter and newClass for the second. Doing so swaps oldClass for newClass.

Note

String.replace() and other wrapper methods for strings were covered in Chapter 2.

There it is. Now let's try swapping "blue" for "fuchsia" for the <ul> element with the help of swapClass(). Verify your work with Figure 8-7:

function swapClass(element, oldClass, newClass) {
  var re = new RegExp("\b" + oldClass + "\b", "g");
  element.className = element.className.replace(re, newClass);
}
swapClass(document.getElementsByTagName("ul")[0], "blue", "fuchsia");
Testing swapClass() on the <ul> element

Figure 8-7. Testing swapClass() on the <ul> element

Scripting Rules

In addition to scripting className, another way to change the appearance of several elements is to script a rule that targets them. Rather than swapping className from "blue" for "fuchsia" for the <ul>, we could just as well change the rule for the blue class in eight.css.

Now that we have our marching orders, refresh Firefox, and clear Firebug. JavaScript represents a style sheet like eight.css with a CSSStyleSheet object. Those implement the members listed in two DOM interfaces, StyleSheet and CSSStyleSheet. The following seven members come from StyleSheet:

disabled
href
media
ownerNode
parentStyleSheet
title
type

Additionally, four CSS-only members come from the CSSStyleSheet interface:

cssRules
ownerRule
deleteRule()
insertRule()

Internet Explorer, of course, deviates from the DOM standard but not by much for StyleSheet. Internet Explorer renames ownerNode, which refers to the <link> or <style> element for the style sheet, as owningElement. For the other StyleSheet members, Internet Explorer abides by the DOM names.

On the other hand, Internet Explorer does not implement any members from CSSStyleSheet. But don't worry. There are Internet Explorer-only members that will enable us to muddle through. The ones we will explore are as follows:

rules
imports
addRule()
addImport()
removeRule()
removeImport()
cssText

Things could be worse, you know. Internet Explorer deviating from DOM makes scripting style sheets tough. But the proprietary workarounds make it doable. So, rather than feel sorry for ourselves, let's start moving forward in small steps.

Before we can query a rule, we need to get at the style sheet that contains the rule. One way is to query the sheet member of a <link> or <style> element in Firefox, Safari, and Opera. Internet Explorer deviates from DOM, no surprise, and implements a proprietary styleSheet member instead. With this in mind, our first steps will be the following:

var myStyleSheet = document.getElementsByTagName("link")[0];
myStyleSheet = myStyleSheet.sheet || myStyleSheet.styleSheet;

Another, less reliable way to query a style sheet is by way of document.styleSheets, which contains an array-like object with one member for every <style> or <link> element having a rel attribute value set to "stylesheet". Internet Explorer and Opera also add one member for every <style> or <link> element having a rel attribute value set to "alternate stylesheet". No browser adds imported style sheets to document.styleSheets—just those included with a <link> or embedded in a <style>. Anyway, we could rewrite the previous sample like so:

var myStyleSheet = document.styleSheets[0];

Note

document.styleSheets is provided by the DocumentStyle interface. In turn, styleSheets contains an array-like object provided by the StyleSheetList interface. Members contained by a StyleSheetList are objects that have the features listed in the StyleSheet and CSSStyleSheet interfaces. So, there are four interfaces for you to consult in a DOM reference.

Now myStyleSheet contains an object representing eight.css. Let's find the rule for the blue class. Where would that be? It's in a cssRules member for Firefox, Safari, and Opera, but in a rules member for Explorer. Those are both CSSRuleList objects. Those are array-like objects, which is to say their members are elements. Moreover, cssRules.length or rules.length contains the number of members.

cssRules contains both styling rules and @ directives like @import. On the other hand, rules contains only styling rules. In other words, it contains the ones comprised of a selector and declaration block. So if a style sheet contains @ directives, cssRules.length will be greater than rules.length. In Internet Explorer, @import directives are nowhere to be found, but any @page directives are in an array-like object named pages. Note that although @import directives are missing in Internet Explorer, scripting imported style sheets remains doable by way of imports, addImport(), and removeImport(). Later in the day, we will explore those features. Note that Internet Explorer splits grouped selectors into more than one rule. Therefore, for the following rule, Internet Explorer would add two members to rules, while Firefox, Safari, and Opera would add one member to cssRules:

div#mast form, div#mast h1 {
  display:inline;
}

In other words, Internet Explorer separates the previous rule into two like so:

div#mast form {
  display:inline;
}
  div#mast h1 {
  display:inline;
}

With all those variations between cssRules and rules in mind, obviously do not hard-code the index of a rule, because it is apt to be wrong inInternet Explorer or in Firefox, Safari, and Opera.

Now, with length and either cssRules or rules, we can loop through the styling rules in eight.css. Note that in any browser, rules are numerically indexed in source code order. Say we want to find the rule with the selector "ul.blue a" in order to change the sprite to fuchsia; a for loop like the following would work cross-browser:

var myStyleSheet = document.getElementsByTagName("link")[0];
myStyleSheet = myStyleSheet.sheet || myStyleSheet.styleSheet;
var myRules = myStyleSheet.cssRules || myStyleSheet.rules;
for (var i = myRules.length - 1; i > −1; i --) {
  if (myRules[i].selectorText && myRules[i].selectorText.toLowerCase() === "ul.blue a") {
    myRules[i].style.backgroundImage = "url(images/fuchsia.gif)";
    break;
  }
}

Three things in the for loop bear explaining. Thought I'd leave you in the lurch, didn't you?

  • First, we loop through the rules in reverse order. Now why would we do that? CSS precedence: in the event that more than one rule in the style sheet has the selector we are looking for, we want to change the last one.

  • Second, @import and other @ rules do not define a selectorText or style member. Calling String.toLowerCase() on undefined returns an error. So, we use the && operator to skip any rules that do not define selectorText.

  • Third, the string in selectorText may or may not match the CSS selector. For this reason, we lowercase selectorText prior to comparing it with ===.

Note

If you have forgotten how the && and === operators work, flip back to Chapter 3.

Now let's see whether everything goes according to plan. Refresh Firefox to revert the sprite to blue. Then click Run in Firebug, and verify your work with Figure 8-8.

Manually finding a rule by its selector

Figure 8-8. Manually finding a rule by its selector

Finding a rule certainly takes some doing. And it's something we're likely to do often. So, maybe we ought to write a helper function to simplify things. Yup, that's what we'll do. Refresh Firefox, clear Firebug, and then declare a function named findRule() that takes two parameters. element will contain the <link> or <style>element, and selector will contain the selector for the rule we want to find.

function findRule(element, selector) {
}

So, an Element node and selector string will go in one end of findRule(), and then a CSSStyleRule object will pop out of the other. Great. Now let's make it happen.

First, compensate for Internet Explorer deviating from DOM by renaming sheet and cssRules. To do so, create a couple local variables like so. Then lowercase the string in the selector argument. This will save your bacon in the event you forget to pass in the selector in lowercase to begin with.

function findRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
}

Now findRules() has the values necessary to find the desired rule. Let's tweak the for loop from the previous sample; the body of the loop just needs to return the matching CSSStyleRule object. In case there are none, return null following the for loop. Recall from earlier in the book that a function that typically returns an object ought to return null (which conveys no object) on failure rather than undefined (which conveys no string, number, or boolean).

With those brave words, our work is done:

function findRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
  for (var i = rules.length - 1; i > −1; i --) {
    if (rules[i].selectorText && rules[i].selectorText.toLowerCase() === selector) {
      return rules[i];
    }
  }
  return null;
}

Now let's see whether findRule() works as planned. Refresh Firefox to revert the sprite to blue. Then in Firebug, swap the blue sprite for the fuchsia sprite with the help of findRule(), verifying your work with Figure 8-9.

function findRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
  for (var i = rules.length - 1; i > −1; i --) {
    if (rules[i].selectorText && rules[i].selectorText.toLowerCase() === selector) {
      return rules[i];
    }
  }
  return null;
}
var myRule = findRule(document.getElementsByTagName("link")[0], "ul.blue a");
if (myRule !== null) {
  myRule.style.backgroundImage = "url(images/fuchsia.gif)";
}
Swapping the blue sprite for the fuchsia one with findRule()

Figure 8-9. Swapping the blue sprite for the fuchsia one with findRule()

Remember from earlier in the chapter that CSSStyleRule.style contains an object with the members listed in the CSSStyleDeclaration and CSS2Properties interfaces. We just wrote CSS2Properties.backgroundImage. Now by way of CSSStyleDeclaration.cssText, let's change the sprite from blue to green for the Saucony link and slide it to the down position. To do so, refresh Firefox, and rework the previous sample like so. Then click Run.

function findRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
  for (var i = rules.length - 1; i > −1; i --) {
    if (rules[i].selectorText && rules[i].selectorText.toLowerCase() === selector) {
      return rules[i];
    }
  }
  return null;
}
var myRule = findRule(document.getElementsByTagName("link")[0], "a#saucony");
if (myRule !== null) {
  myRule.style.cssText = "background:url(images/green.gif) −99px −135px; top:205px";
}

One thing to note or reiterate is that changing cssText is draconian, obliterating any declarations in there. This is why we had to reset top to 205px in the previous sample. Another, more general thing to note is that a style sheet differs from a <link> or <style>. The latter is markup, while the former is CSS. In other words, a style sheet is the CSS code in the file included by a <link> element or contained by a <style> element. So, you cannot, for example, retrieve a style sheet by its id member, since it does not have one. However, you can and probably should retrieve the corresponding <link> or <style> element by its id. Doing so ensures that your script continues to work in the event that CSS designers add more <link> or <style> tags. It's also the case if you add more tags dynamically by script, which we will cover not far down the road.

For example, rather than hoping that the first <link> element continues to be the one we want to script in the future, we ought to modify the previous sample like so:

function findRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
  for (var i = rules.length - 1; i > −1; i --) {
    if (rules[i].selectorText && rules[i].selectorText.toLowerCase() === selector) {
      return rules[i];
    }
  }
  return null;
}
var myRule = findRule(document.getElementById("spriteStyles"), "a#saucony");
if (myRule !== null) {
  myRule.style.cssText = "background:url(images/green.gif) −99px −135px; top:205px";
}

Scripting Imported Style Sheets

Now then, what if you want to script an imported style sheet? In other words, say you want to script one included with an @import directive. First, save eight.html as running_copy.html; then, replace the <link> element with a <style> element that imports eight.css like so:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Scripting CSS</title>
<style type="text/css">
@import url(eight.css);
</style>
</head>
<body>
<div id="running">
  <h4>Running</h4>
  <ul class="blue">
    <li><a id="adidas" href="http://www.adidas.com">adidas</a></li>
    <li><a id="asics" href="http://www.asics.com">ASICS</a></li>
    <li><a id="brooks" href="http://www.brooksrunning.com">Brooks</a></li>
    <li><a id="newBalance" href="http://www.newbalance.com">New Balance</a></li>
    <li><a id="nike" href="http://www.nike.com">Nike</a></li>
<li><a id="saucony" href="http://www.saucony.com">Saucony</a></li>
  </ul>
</div>
</body>
</html>

As noted earlier in the day, an @import rule does not have a selectorText or style member. The reason for this is that an @import rule does not implement the CSSStyleRule interface but instead implements CSSImportRule. This interface provides three members:

href
media
styleSheet

StyleSheet refers the imported style sheet. So if we want to change left to 500px for the running <div>, we can do so by entering and running the following in Firebug:

document.getElementsByTagName("style")[0].sheet.cssRules[0].styleSheet.cssRules[2].style.left
= "500px";

This works for Firefox, Safari, and Opera. But it doesn't work for Internet Explorer, which does not implement CSSImportRule. Rather, for a <style> or <link> element, styleSheet.imports contains a collection of imported style sheets. So, we would rework the previous sample like so:

document.getElementsByTagName("style")[0].styleSheet.imports[0].rules[2].style.left = "500px";

Adding or Deleting a Rule

Now then, what if you want to dynamically determine the numeric index of a rule cross-browser? Can it be done? Yup. But why would you want to know a rule's index? A couple reasons: to add a rule or delete one. So, before we try explore those operations, let's figure out how to determine the numeric index of a rule regardless of the visitor's browser.

Insofar as this is something we will want to frequently do, you probably know what I am going to say. Right, let's code a helper function. So, delete running_copy.html and reload eight.html in Firebug. Then key in the following function, which differs from findRule() in just two ways:

  • First, it returns the loop variable i rather than a CSSStyleRule object. i will be the index of the desired rule. That was painless.

  • Second, to convey failure in the event no rule in the style sheet has the selector we gave findIndex() to work with, the return value will be undefined rather than null. Why? findIndex() ought to return a number. Those are stored on the stack, and undefined conveys no value on the stack. On the other hand, findRule() ought to return a object. Those are stored on the heap, and null conveys no value on the heap. You forgot about all that, didn't you?

function findIndex(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
  for (var i = rules.length - 1; i > −1; i --) {
    if (rules[i].selectorText && rules[i].selectorText.toLowerCase() === selector) {
      return i;
    }
  }
}

Let's see whether findIndex() works as planned. Refresh Firefox to revert the sprite to blue. Then pass in "a#adidas" for selector, and click Run. Since that is the tenth rule in eight.css, the return value ought to be 9 because JavaScript numbers them beginning with 0. Verify your work with Figure 8-10.

function findIndex(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  selector = selector.toLowerCase()
  for (var i = rules.length - 1; i > −1; i --) {
    if (rules[i].selectorText && rules[i].selectorText.toLowerCase() === selector) {
      return i;
    }
  }
}
findIndex(document.getElementById("spriteStyles"), "a#adidas");
// 9
Finding the numeric index of the rule with the "a#adidas" selector with findIndex()

Figure 8-10. Finding the numeric index of the rule with the "a#adidas" selector with findIndex()

Adding a Rule to a Style Sheet

Now that we have a helper function to provide us with the numeric index of a rule, we can explore how to insert a rule into a style sheet. Of course, there is a DOM way and an Internet Explorer way. Let's write another helper function to compensate for Internet Explorer's skullduggery.

Clear Firebug, but do not refresh Firefox because we want findIndex() to remain in memory. Name the helper function insertRule. This one will work with four parameters:

  • element will be a <link> or <style>Element node.

  • selector will be the text of the selector for the new rule, in other words, a string like "div#running li".

  • declarations will be the text of the declaration block, minus the curly braces, such as a string like "top:135px; background-position:0 −81px". Note that property names are dash case. It's just like in your CSS code or in CSSStyleDeclaration.cssText.

  • index contains the text of the selector for the rule you want to insert the new rule before. So, that's the selector string we will pass to findIndex(), which will then return a numeric index, or undefined if we are out of luck.

function insertRule(element, selector, declarations, index) {
}

Now let's move on to the body of our helper function. Assign either the DOM or Internet Explorer member that contains the CSSStyleSheet object to a local variable named sheet. Similarly, assign the DOM or Internet Explorer member that contains the CSSRuleList array-like object to one named rules. It's just like we did in findIndex().

Now let's make the index parameter optional by way of a couple if statements. JavaScript will run the block of the first if statement in the event that we did pass a selector string for the value of index. Otherwise, JavaScript will run the block of the second if statement, like if we invoked insertRule() with just three parameters rather than four. In this case, index defaults to undefined. Let's overwrite that value with rules.length, which contains a number one greater than the total number of rules in the style sheet. Later, this numeric index will enable us to append the new rule to the very end of the style sheet. Thus far we have this:

function insertRule(element, selector, declarations, index) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  if (typeof index === "string") {
    index = findIndex(element, index);
  }
  if (typeof index !== "number") {
    index = rules.length;
  }
}

Now sheet will contain either a DOM method named insertRule() or an Internet Explorer method named addRule(). Let's figure out which one is available by way of the else if idiom. In the event that Firefox, Safari, or Opera is running our function, insertRule() will be defined. This method takes two parameters:

  • The full text of the rule, curly braces and all. So, we will cobble that together with the + operator.

  • The numeric index of the new rule. We have that number on hand in the index, so just pass that in, and we're done.

Now let's code a path for Internet Explorer to run. Its method, addRule(), takes three parameters: the text for the selector, the text for the declarations, and the numeric index for where to insert the rule. Simply pass in the values of selector, declarations, and index. It's simple as can be:

function insertRule(element, selector, declarations, index) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  if (typeof index === "string") {
    index = findIndex(element, index);
  }
  if (typeof index !== "number") {
    index = rules.length;
  }
  if (sheet.insertRule) {
    sheet.insertRule(selector + "{" + declarations + "}", index);
  } else if (sheet.addRule) {
    sheet.addRule(selector, declarations, index);
  }
}

Between the rules with the selectors "ul.blue a" and "ul.fuchsia a", let's insert a new one to swap the sprite from blue to fuchsia by calling insertRule() like so. Verify your work with Figure 8-11.

function insertRule(element, selector, declarations, index) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  if (typeof index === "string") {
    index = findIndex(element, index);
  }
  if (typeof index !== "number") {
    index = rules.length;
  }
  if (sheet.insertRule) {
    sheet.insertRule(selector + "{" + declarations + "}", index);
  } else if (sheet.addRule) {
    sheet.addRule(selector, declarations, index);
  }
}
insertRule(document.getElementById("spriteStyles"), "ul.blue a",
  "background-image:url(images/fuchsia.gif)", "ul.fuchsia a");
Inserting a rule with our helper functions, findIndex() and insertRule()

Figure 8-11. Inserting a rule with our helper functions, findIndex() and insertRule()

Of course, since the final rule in a style sheet clobbers any similar ones appearing earlier, we could just as well omitted the fourth parameter. Our function will then append our new rule to the very end of the rule collection. Take advantage of that feature to swap the sprite from fuchsia to green like so:

function insertRule(element, selector, declarations, index) {
  var sheet = element.sheet || element.styleSheet;
  var rules = sheet.cssRules || sheet.rules;
  if (typeof index === "string") {
    index = findIndex(element, index);
  }
  if (typeof index !== "number") {
    index = rules.length;
  }
  if (sheet.insertRule) {
    sheet.insertRule(selector + "{" + declarations + "}", index);
  } else if (sheet.addRule) {
    sheet.addRule(selector, declarations, index);
  }
}
insertRule(document.getElementById("spriteStyles"), "ul.blue a",
  "background-image:url(images/green.gif)");

Deleting a Rule from a Style Sheet

Now then, let's draft a helper function to delete a rule from a style sheet. Just as with adding a rule, Internet Explorer implements a proprietary method instead of the one from the DOM standard. Our game plan for this helper function will be similar to the one for adding a rule. In other words, we will have Firefox, Safari, and Opera invoke the DOM method, deleteRule(), and Internet Explorer will invoke its proprietary one, removeRule(). There will be one path for standard savvy browsers and one path for dummy Internet Explorer.

Clear Firebug, but do not refresh Firefox because we want to keep findIndex() in memory. Name the helper function deleteRule. It's the same identifier as for the DOM method. Now why do those identifiers not collide?

Take your time.

This is one you ought to know.

By the way, we named the helper function for adding a rule with the identifier for the DOM method, insertRule. Those didn't collide either.

What do you think?

Yup, different namespaces. The helper functions insertRule() and deleteRule() are methods of window. On the other hand, the DOM functions insertRule() and deleteRule() are methods of a CSSStyleSheet object. They're in different folders, so to speak.

Where were we? Hmm. OK, now define two named arguments:

  • The first one, element, will be a <link> or <style>Element node.

  • The second one, selector, will be the text of the selector for the rule to delete.

Insofar as the DOM and Internet Explorer functions for deleting a rule are methods of a CSSStyleSheet object, we need to save a reference to the one containing the rule to delete. Put that in a local variable named sheet. Then save the return value of passing element and selector to findIndex() to another local variable named index. If findIndex() has no luck with those parameters and has to return undefined instead of a number, we cannot delete a rule—not unless we want to do so randomly! So, write an if statement to abort deleteRule() in the event we have no numeric index to work with.

Thus far we have this:

function deleteRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var index = findIndex(element, selector);
  if (typeof index !== "number") {
    return;
  }
}

Now for the mojo. Both the DOM and Internet Explorer methods work with one parameter, the numeric index of the rule to delete. Just as a reminder, indexes are integers beginning with 0. So, code two paths with the if else idiom. Firefox, Safari, and Opera go down the first path, and Internet Explorer goes down the second. We're done:

function deleteRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var index = findIndex(element, selector);
  if (typeof index !== "number") {
    return;
}
  if (sheet.deleteRule) {
    sheet.deleteRule(index);
  } else if (sheet.removeRule) {
    sheet.removeRule(index);
  }
}

Now let's try to delete a couple rules, such as the two we just added with insertRule(). Those both have a selector of "ul.blue a". The first time we pass "ul.blue a" to deleteRule(), the sprite turns from green to fuchsia, and the second time, it turns from fuchsia to blue. So, run the following twice in Firebug, verifying your work with Figure 8-12:

function deleteRule(element, selector) {
  var sheet = element.sheet || element.styleSheet;
  var index = findIndex(element, selector);
  if (typeof index !== "number") {
    return;
  }
  if (sheet.deleteRule) {
    sheet.deleteRule(index);
  } else if (sheet.removeRule) {
    sheet.removeRule(index);
  }
}
deleteRule(document.getElementById("spriteStyles"), "ul.blue a");
Deleting the two newly inserted rules with deleteRule() swaps the sprite from green to fuchsia to blue.

Figure 8-12. Deleting the two newly inserted rules with deleteRule() swaps the sprite from green to fuchsia to blue.

Querying Overall Styles from the Cascade

Now then, what if you want to query the overall styles from the CSS cascade for an element? In other words, you want to know the declarations that have the highest precedence. Those form one humongous declaration block, which, like the declaration block for a rule, JavaScript represents with a CSSStyleDeclaration object. You already know how to query those cumulative declarations—the . operator and a CSS2Properties member. Remember they are camel case, not dash case. On the other hand, CSSStyleDeclaration.cssText contains the declarations block, minus the curly braces, as a string. However, this string is oftentimes too lengthy to bother with.

The cumulative declaration block from the cascade differs from that of a rule in a style sheet in a few ways:

  • It is read-only. Try to assign a new value to a property from the cascade, and JavaScript calls you a dummy by way of an error.

  • Any relative values are converted to absolute values. Typically this means converting the value to pixels.

  • Any property that sets several properties in one fell swoop, such as a margin or border, may contain undefined, while their corresponding fully expanded properties, such as marginLeft or borderRightStyle, will always contain a value. As a rule of thumb, do not query rollup values from the cascade. Query their fully expanded equivalents instead.

Refresh Firefox, clear Firebug, and let's cobble together a helper function named queryCascade() that works with two parameters:

  • element will contain an Element node for the DOM tree.

  • property will contain the name of the CSS property as a camel case string. That is to say, don't pass a dash case string or a camel case identifier.

Now let's send Firefox, Safari, and Opera down one path and Internet Explorer down another by way of an if statement and the else if idiom. First let's test for the DOM function getComputedStyle() by way of the typeof operator. If typeof returns "function" rather than "undefined", invoke with two parameters, element and null. getComputedStyle() will then return a CSSStyleDeclaration object containing the cumulative declarations from the cascade, as well as those implicitly calculated by the browser, for element. Then query property by way of the [] operator.

function queryCascade(element, property) {
  if (typeof getComputedStyle === "function") {
    return getComputedStyle(element, null)[property];
  }
}

Do you remember why doing so with the . operator will not work?

Come on, you know this one.

I'm not telling you the answer.

Maybe I'll go reorganize the Penske file while you think it through.

Note

George reorganized the Penske file in the Seinfeld episode "The Barber."

What do you think? Right, the . operator works with a hard-coded identifier. On the other hand, [] works with a string or any expression that evaluates to a string, most notably a variable or parameter.

Note

The if statement and else if idiom were covered in Chapter 4, while the typeof and refinement operators were covered in Chapter 3. So if you have forgotten how those work, take a moment to flip back and refresh your memory. I'll wait for you to return before continuing. Promise!

Why do we use null for the second parameter? This can be a pseudo-element as a string, for example, ":before" or ":after". However, Internet Explorer's proprietary alternative to getComputedStyle() does not support pseudo-elements. Therefore, pass null to make things work cross-browser.

Now for our DOM dummy, Internet Explorer, which does not implement the ViewCSS interface that provides getComputedStyle(). Internet Explorer's way is simple: an Element node has not only a style member for inline styles but also a currentStyle member for cascade styles. Both style and currentStyle contain a CSSStyleDeclaration object. TheInternet Explorer path is straightforward, and we're done coding queryCascade().

function queryCascade(element, property) {
  if (typeof getComputedStyle === "function") {
    return getComputedStyle(element, null)[property];
  } else if (element.currentStyle) {
    return element.currentStyle[property];
  }
}

Now let's query a CSS property from the cascade. Then we have a couple that were calculated behind the scenes by Firefox:

function queryCascade(element, property) {
  if (typeof getComputedStyle === "function") {
    return getComputedStyle(element, null)[property];
  } else if (element.currentStyle) {
    return element.currentStyle[property];
  }
}
queryCascade(document.getElementById("adidas"), "width");
// "100px"
queryCascade(document.getElementsByTagName("ul")[0], "width");
// "120px"
queryCascade(document.getElementsByTagName("ul")[0], "height");
// "0px"

For the Adidas <a> element, queryCascade() returned the value from an explicit declaration in eight.css. On the other hand, the values for the <ul> element were calculated behind the scenes for us by Firefox. Note that insofar as the <ul> is a block element, its width is set to that of the containing <div>, which we set to "120px". On the other hand, since the <a> elements within the <ul> are absolutely positioned relative to the <div> and the <li> has a display value of inline, the <ul> collapses to a height of "0px".

Here is the situation. Nowadays, CSS presentation and markup content are in separate files. Initially, the CSSStyleDeclaration object in the style member for an Element node will be irrelevant, with just scores of "" empty strings and other default values. On the other hand, the CSSStyleDeclaration object representing cascade declarations is totally relevant but read-only. So if you do not know what rule or style sheet contains the declaration with the highest precedence, then what do you do? Typically, scripters read the cascade and write the inline style attribute. That is to say, they query two separate CSSStyleDeclaration objects. Obviously, if you know where to find the rule, then you simply read and write the CSSStyleDeclaration object representing the rule's declaration block, like we did earlier in the day. But more often than not, you won't have that option.

Making the computed styles from the cascade read-only is a DOM failing. But to be fair, the standard predates the notion of putting markup, CSS, and JavaScript in separate layers by five years. Maybe someday the flaw will be fixed. But that day is far off and may never come at all. So, let's give the "read the cascade, write the attribute" thing a try.

Clear Firebug, but do not refresh Firefox because we want queryCascade() to remain available in memory. Then save the <div> to a variable named myDIV. Pass myDIV and "left" to queryCascade(), which will then return "40px". Convert that string to the number 40 by way of parseInt(), and then add 10. Next, convert the sum of 50 to the string "50", and append "px". Finally, write that string, "50px", to the style attribute. Click Run repeatedly until left is set to "700px". JavaScript will move our running shoe menu to the right by 10 pixels each time you click Run.

Note

If you have forgotten how parseInt() works, flip back to Chapter 2. Datatype conversion is covered there, too.

function queryCascade(element, property) {
  if (typeof getComputedStyle === "function") {
    return getComputedStyle(element, null)[property];
  } else if (element.currentStyle) {
    return element.currentStyle[property];
  }
}
var myDIV = document.getElementById("running");
myDIV.style.left = parseInt(queryCascade(myDIV, "left")) + 10 + "px";

Verify your work with Figure 8-13.

Moving the interface to the right in 10-pixel increments with the help of queryCascade()

Figure 8-13. Moving the interface to the right in 10-pixel increments with the help of queryCascade()

JavaScript animations typically work in this way – that is to say, by writing the style attribute at regular intervals. Later in the book, we will do just that.

Enabling and Disabling Style Sheets

Now then, sometimes you will want to turn a style sheet on or off. Maybe you'll want to change the skin for an interface, for example. Doing so is straightforward and even works in our DOM dummy, Internet Explorer. Every style sheet embedded in a <style> element or included by a <link> element has a disabled member (whichcomes from the CSSStyleSheet interface, by the way).

Now do you remember the JavaScript datatype to convey on or off?

It's the same as the one that conveys yes or no.

Come on, you know this one.

Right, the booleans, true and false. The only tricky thing with the disabled member is that it isn't named enabled. In other words, true means the style sheet is off, and false means that the style sheet is on. Trust me, you will get this mixed up from time to time.

Refresh Firefox, and clear Firebug. Then disable eight.css, entering and running the following helper function and verifying your work with Figure 8-14:

function toggleSheet(element) {
  var sheet = element.sheet || element.styleSheet;
  sheet.disabled = ! sheet.disabled;
}
toggleSheet(document.getElementById("spriteStyles"));
Disabling a style sheet by setting its disabled member to true

Figure 8-14. Disabling a style sheet by setting its disabled member to true

Our markup is totally unstyled now. No, I don't like it either. Click Run again to have Firefox reapply eight.css.

Including or Importing Style Sheets

At this point, there's no disabled attribute in the markup for a <link> or <style> tag. disabled is a member of an Element node but not an attribute. Note that we covered difference between a member and an attribute in Chapter 7; flip back to that chapter if you have forgotten.

Inasmuch as disabled is not a markup attribute, we can set its value to true only with JavaScript. If JavaScript is not available, we cannot turn off a style sheet by calling toggleSheet(). But what if we want a style sheet to be applied to a document only when JavaScript is available? Simple. We create and insert a new <link> or <style>Element node. However, like everything else with scripting CSS, there are Internet Explorer pitfalls. So, let's code a helper function to do the job.

Refresh Firefox, and clear Firebug. Then name the helper function addSheet, defining two arguments:

  • tag will contain the string "link" or "style".

  • url will contain the URL of the style sheet.

Next, pass tag to document.createElement(), saving the return value to a local variable named element. Regardless of whether element now contains a <link> or <style>, the type attributes will be "text/css". Thus far, we have this:

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
}

Now append element to childNodes for <head> by way of appendChild(), which we covered in Chapter 7. We have to do this now for the remainder of the function to work cross-browser.

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
}

In the event we passed "link" for tag, set rel to "stylesheet" and href to the string in url. The browser will then apply the style sheet to the document. So for a <link>, we're done.

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
  if (tag === "link") {
    element.rel = "stylesheet";
    element.href = url;
  }
}

Things are trickier if tag contains "style". For Firefox, Safari, and Opera, we just cobble together an @import directive and pass that to insertRule(). For Internet Explorer, you might think to insert the directive with addRule(). But you would be wrong. Internet Explorer has a separate method named addImport() for inserting @import directives. addImport() works with two parameters:

  • The URL of the style sheet to import. Just pass in the URL for the first parameter; there's no need to cobble together an @import directive.

  • The second parameter, which is optional, is the numeric index for where to insert the directive in imports. Remember from earlier in the chapter that imports is where Explorer puts @import directives. That is to say, those are kept separate from the rule sets in rules.

Let's code these two paths by way of a try catch statement. The try block will work in Firefox, Safari, and Opera but will generate an error in Internet Explorer, which will then go down the catch path, merrily as can be. Our final code for addSheet() is as follows:

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
document.getElementsByTagName("head")[0].appendChild(element);
  if (tag === "link") {
    element.rel = "stylesheet";
    element.href = url;
  } else if (tag === "style") {
    try {
      element.sheet.insertRule("@import url(" + url + ")", 0);
    } catch (whyNot) {
      element.styleSheet.addImport(url);
    }
  }
}

So that we can tangibly test addSheet(), let's first delete the <link> that includes eight.css. Remember how to do so from the previous chapter? Yup, with removeChild(). Just invoke removeChild() on the <head>element and pass in the <link>, and Firefox will redisplay our document as unstyled markup, as Figure 8-15 illustrates.

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
  if (tag === "link") {
    element.rel = "stylesheet";
    element.href = url;
  } else if (tag === "style") {
    try {
      element.sheet.insertRule("@import url(" + url + ")", 0);
    } catch (whyNot) {
      element.styleSheet.addImport(url);
    }
  }
}
document.getElementsByTagName("head")[0].
  removeChild(document.getElementsByTagName("link")[0]);
Delete the <link> element with removeChild(), and Firefox redisplays the document as unstyled markup.

Figure 8-15. Delete the <link> element with removeChild(), and Firefox redisplays the document as unstyled markup.

By the way, even though it may be obvious, note that passing a <link> or <style> to removeChild() does the inverse of what addSheet() does. So, removing a style sheet is simpler than adding one.

Now that our document is totally unstyled, we can tangibly test addSheet(). Let's first try to include eight.css with a new <link>. Go ahead and delete the removeChild() invocation. Then call addSheet() like so, verifying your work with Figure 8-16.

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
  if (tag === "link") {
    element.rel = "stylesheet";
    element.href = url;
  } else if (tag === "style") {
    try {
      element.sheet.insertRule("@import url(" + url + ")", 0);
    } catch (whyNot) {
      element.styleSheet.addImport(url);
    }
}
}
addSheet("link", "eight.css");
Dynamically including eight.css with addSheet()

Figure 8-16. Dynamically including eight.css with addSheet()

Did Firefox restyle your document with the blue sprite? Great. Now let's again remove the <link> with removeChild(). Then insert a <style> with an @import directive with the help of addSheet(). Doing so restyles the document, too.

function addSheet(tag, url) {
  var element = document.createElement(tag);
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
  if (tag === "link") {
    element.rel = "stylesheet";
    element.href = url;
  } else if (tag === "style") {
    try {
      element.sheet.insertRule("@import url(" + url + ")", 0);
    } catch (whyNot) {
      element.styleSheet.addImport(url);
}
  }
}
document.getElementsByTagName("head")[0].
  removeChild(document.getElementsByTagName("link")[0]);addSheet("style", "eight.css");

Embedding a Style Sheet

Sometimes you may want to embed a style sheet in a new <style> element rather than including or importing one. Insofar as the newly minted <style> is empty, the simplest way to embed a style sheet is to create a Text node containing all the rules and then insert it into the <style> with appendChild(). It works fine for Firefox, Safari, and Opera, but not for Internet Explorer. There's a workaround, though. In Internet Explorer, styleSheet has a cssText member. Rather than create a Text node from our string of CSS rules, we will simply assign the string to cssText. With those brave words, let's code a helper function named embedSheet(). This one works with one parameter, a string of CSS rules. The fork in the road where Firefox, Safari, and Opera and Internet Explorer part company will be formed from a try catch statement:

function embedSheet(text) {
  var element = document.createElement("style");
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
  text = document.createTextNode(text);
  try {
    element.appendChild(text);
  } catch (whyNot) {
    element.styleSheet.cssText = text.data;
  }
}

Now let's test embedSheet() by passing in a couple of rules that will change the sprite from blue to fuchsia and move the interface to the right. Enter and run the following, and then verify your work with Figure 8-17:

function embedSheet(text) {
  var element = document.createElement("style");
  element.type = "text/css";
  document.getElementsByTagName("head")[0].appendChild(element);
  text = document.createTextNode(text);
  try {
    element.appendChild(text);
  } catch (whyNot) {
    element.styleSheet.cssText = text.data;
  }
}
embedSheet("ul.blue a {background-image:url(images/fuchsia.gif);} div#running {left:500px;}");
Dynamically embedding a style sheet with embedSheet()

Figure 8-17. Dynamically embedding a style sheet with embedSheet()

Summary

In this chapter, we explored a number of DOM interfaces for scripting CSS, while in Chapter 7 we explored the interfaces for scripting markup. Now you know how to do what JavaScript behaviors do. You can change the presentation or content of a document. Controlling when those changes occur is the topic of the next chapter. There we will explore event interfaces provided by DOM and Internet Explorer. See you there!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.167.195