Enhancing Structural Markup with JavaScript

Just a few years ago, the key skill involved in writing HTML was knowing enough table tricks to persuade the two major browsers to do more or less what you wanted them to. The modern Web is a very different beast, where the quality of your markup is judged on the basis of how well you use structural elements such as headers, paragraphs and lists to describe your content.

The benefits of this approach have been explained many times before: more maintainable code, smaller file sizes, better accessibility and the ability to control your site’s look and feel from a single style sheet rather than hacking away at sprawling chunks of markup spread across multiple pages. An advantage that is not so frequently discussed is that well structured markup opens the door for additional site enhancements based around that long-abused third leg of the client-side Web, Javascript.

This article will look at two ways in which Javascript and well structured markup can work together. The first example will show how Javascript can enhance a blockquote by hooking into its cite attribute. The second will demonstrate a “best of breed” script for building links that toggle which panel is visible on a page.

Blockquote Citations

For our first example, let’s take a look at the humble blockquote element. Often misused to apply indentation, the correct usage of this element is to mark up quotations that should appear visually separate from surrounding text. The opening blockquote tag can take an optional cite attribute, which should contain the URL of the page on which the quotation originated.

The only problem with the cite attribute is that browsers completely ignore it. Markup purists may appreciate it, but, from a purely practical point of view, nothing is gained by using it save a feeling of smug satisfaction at using the correct markup. This is where Javascript comes in. Using the DOM, it is possible to add a link to the quotation source at the bottom of any blockquote that has a cite attribute. Here’s the code for a function that does just that:

function extractBlockquoteCitations() { 
  var quotes = document.getElementsByTagName('blockquote'); 
  for (var i = 0; i < quotes.length; i++) { 
    var cite = quotes[i].getAttribute('cite'); 
    if (cite != '') { 
      var a = document.createElement('a'); 
      a.setAttribute('href', cite); 
      a.setAttribute('title', cite); 
      a.appendChild(document.createTextNode('Source')); 
      var p = document.createElement('p'); 
      p.className = 'blockquotesource'; 
      p.appendChild(a); 
      quotes[i].appendChild(p); 
    } 
  } 
}

Let’s take a close look at the body of the function.

var quotes = document.getElementsByTagName('blockquote');

This line uses the DOM method, getElementsByTagName, to find all of the blockquote elements in the current page, assigning them to an array (it’s actually an HTMLCollection, but conveniently this is a data structure that behaves like an array) called quotes.

for (var i = 0; i < quotes.length; i++) { 
  var cite = quotes[i].getAttribute('cite'); 
  if (cite != '') {

Now we’re looping through the gathered blockquote nodes. Each time, we use the getAttribute method to retrieve the cite attribute from the element. If the cite attribute has been set, we go on to the fun part: creating a “Source” link at the bottom of the quotation.

    var a = document.createElement('a'); 
    a.setAttribute('href', cite); 
    a.setAttribute('title', cite);

When we want dynamically to add new HTML elements to a page using the DOM, the correct way of doing so is to create the elements programmatically using the createElement method. The above lines create a new ‘a’ element and assign it href and title attributes, both set to the URL of the citation.
a.appendChild(document.createTextNode('Source'));

We want the link element to contain some text that the user can click on to activate the link. Raw text nodes are created using the createTextNode method. The DOM treats HTML elements as forming a tree, so to add the text to our newly created link we need to call its appendChild method.

    var p = document.createElement('p'); 
    p.className = 'blockquotesource'; 
    p.appendChild(a);

To allow us to flexibly style the new link using CSS, we can wrap it in a paragraph element. The above code creates such an element, sets its class to ‘blockquotesource‘ to provide a hook for our CSS, then adds the link to it using appendChild. At this point, the new document fragment we have constructed is equivalent to the following HTML:

<p class="blockquotesource"> 
<a href="[cite URL]" title="[cite URL]">Source</a> 
</p>

At the moment, out fragment remains invisible because, while we have created it in memory, we have not yet attached it to our document. The last line in the function does just that:

    quotes[i].appendChild(p);

quotes[i] is the blockquote element we’re currently processing. appendChild attaches our new paragraph to the blockquote, making it visible in the process.

There are two more steps. Firstly, we need the above function to run when the page is first loaded. There are a number of ways of achieving this. The most simple is to add a call to the function to the onload attribute of the document’s body element:

<body onload="extractBlockquoteCitations();">

This works just fine, but we can do better. Since our Javascript function will be hosted in an external file, wouldn’t it make sense for the external file to cause the function to run as well? The naive way of doing that is with the following line of Javascript:

window.onload = extractBlockquoteCitations;
//

Note that we have provided the name of the function, but neglected the () at the end, which would cause the function to execute. Javascript supports the functional programming style, which means that functions can be treated just like any other data object and passed around as arguments, stored in data structures, or even returned from other functions. I will talk about this topic more in a future article, but the upshot of it is that assigning a function to window.onload will cause it to execute once the page has finished loading.

This solution, however, also has a drawback. If you want to use on a given page multiple scripts that execute when the page has finished loading, the last script to register itself with window.onload will be the only script to execute. What’s really needed is a way to attach our function to the onload handler of the window object without overwriting what’s already there. Unfortunately, Internet Explorer and other browsers differ on how this kind of dynamic event attachment should be handled; fortunately, Scott Andrew has released a function that handles these differences for you. Here’s the function:

function addEvent(obj, evType, fn){ 
  if (obj.addEventListener){ 
    obj.addEventListener(evType, fn, false); 
    return true; 
  } else if (obj.attachEvent){ 
    var r = obj.attachEvent("on"+evType, fn); 
    return r; 
  } else { 
    return false; 
  } 
}

And here’s the code to add our blockquotes function to the load event of the window object:

addEvent(window, 'load', extractBlockquoteCitations);

The final step is to style our quotations using CSS. Here’s a relatively simple CSS snippet for handling blockquotes:

blockquote { 
  border-left: 0.25em solid navy;  
  padding: 0 0.5em;  
  margin: 0.5em 1.5em 0.5em 2.5em;  
} 
blockquote p.blockquotesource { 
  font-weight: bold; 
  font-size: 0.8em; 
  text-align: right; 
  padding-top: 0.5em; 
}

The finished product can be viewed here.

 

 

Panel Switching

Now, let’s consider a more advanced dynamic effect — a panel switcher. The objective here is to have a number of panels (marked up using divs) on a page, of which only one is visible at a time. A set of links that remain constantly visible can be used to select which of the panels is currently on display. This could be useful for building something like a tabbed interface for browsing a series of related screens without requiring a page refresh every time one of the tabs is selected.

A good rule to keep in mind whenever Javascript is used to enhance a page is that the page must still be usable with Javascript disabled. In this case, this means that the ideal solution would work as advertised with Javascript turned on, but would display all of the panels on the page in a non-Javascript environment, with each of the links linking directly to the relevant panel, using a URL fragment.

Here, then, is the simplest markup that could possibly work:

<a href="#p1">Panel 1</a> | <a href="#p2">Panel 2</a>  

<div id="p1">This is Panel 1</div>  
<div id="p2">This is Panel 2</div>

Surprisingly, the above is nearly all the markup we need in order to hook in some Javascript to create the desired effect. We could just go ahead and use the above code, but let’s add a class to the links to explicitly state that we wish to do something special with them:

<a href="#p1" class="toggle">Panel 1</a> |   
<a href="#p2" class="toggle">Panel 2</a>
//

Here’s how the Javascript is going to work. When the page loads, the script will scan through all the links on the page looking for any that have “toggle” in their class. For any that are found, the href attribute will be examined, and the element with the ID specified there will be located and added to an array of targeted elements. All but the first of these elements will be “switched off”, so when the page loads, only the first panel will remain visible. The links themselves will have Javascript event handlers attached to them so that, when they’re activated, their corresponding panel can be displayed.

The full script can be viewed here . There follows a walk through of how the code works.

var et_toggleElements = [];

This first line creates a global empty array, which will hold references to the panel elements on the page. Because this script has a global variable and a number of functions, we will be prefixing each with “et_” (for “easy toggle”) — this lowers the chance of our functions suffering name clashes with other scripts loaded by the same page.

/* Initialisation */  
function et_init() {  
  var i, link, id, target, first;  
  first = true;  
  for (i = 0; (link = document.links[i]); i++) {

So far, we’ve initialised some variables, set the first flag to true and started to iterate over all the links in the document. Declaring the variables using var is important because it ensures the variables are local to the function. Without this step, they would be globally accessible and could interfere with other scripts.

    if (/btoggleb/.exec(link.className)) {

This conditional checks that the current link has ‘toggle’ in its class. We’re using a regular expression, rather than just checking if link.className == 'toggle', because the class attribute can contain more than one class, separated by spaces. /btoggleb/ is the regular expression; the b parts match a “word boundary”, which could be a space or the beginning or end of the string.

      id = link.href.split('#')[1];

If the link has toggle in its list of classes, we assume that the target of the link is a URL fragment.
link.href.split('#') splits the link href at the # mark -- we know that the part we're interested in comes after the #, so we directly index the resulting array with [1] to pull out the targeted ID.
      target = document.getElementById(id);  
      et_toggleElements[et_toggleElements.length] = target;

Here, we make another assumption — that the element indicated by the link actually exists. We grab that element using the getElementById() method, then add it on to our array of elements by assigning it to the array index that equals the current length of the array. This works because arrays are indexed from 0, but the array length counts starting from 1; hence, the length of the array is also the index of the next empty slot in the array.

      if (first) {  
        first = false;  
      } else {  
        target.style.display = 'none';  
      }

This is where the first flag we defined earlier makes itself useful. We want the first panel on the site to remain visible, while the others are hidden using the Javascript equivalent of ‘display: none‘ in CSS. The flag allows us to do this.

      link.onclick = et_toggle;  
    }  
  }  
}

 

 

Finally, we assign the et_toggle function to the link’s onclick event, causing the function to be called whenever the links is activated. The next step is to define that function.

function et_toggle(e) {   
  if (typeof e == 'undefined') {   
    var e = window.event;   
  }   
  var source;   
  if (typeof e.target != 'undefined') {   
    source = e.target;   
  } else if (typeof e.srcElement != 'undefined') {   
    source = e.srcElement;   
  } else {   
    return true;   
  }

This first block of code exists to make up for another difference between the way Internet Explorer and other browsers handle events. We need to know which link was activated when the function was called, as this will allow us to identify the panel which should be displayed. The above code identifies the element from which the event originated and places it in the source variable, using code adapted from Peter-Paul Koch’s excellent explanation of the issue on QuirksMode:

  if (source.nodeType == 3) {   
    source = targ.parentNode;   
  }

This code is there for compatibility with Safari. All other browsers that I tested returned the actual link element as the source of the click event, but Safari returned instead the text node contained inside the link. Text nodes have their nodeType set to 3 (a constant defined by the W3C DOM), so, by checking for this, we can identify this problem and reset the target to the parent of the text node, which should be the link element.

  var id = source.href.split('#')[1];
//

Again we extract the ID from the link using the split method.

var elem;
for (var i = 0; (elem = et_toggleElements[i]); i++) {
if (elem.id != id) {
elem.style.display = 'none';
} else {
elem.style.display = 'block';
}
}

Now that we know which panel we wish to display, we can cycle through our array of elements, switching off everything except the one with an ID that matches the ID of the required panel.

  return false;   
}

By returning false, we prevent the link from actually being followed when it is activated, which could result in an undesirable jump down the page viewed in the browser.

The final step is to register the et_init function with the load event of the window, using the addEvent function described earlier.

addEvent(window, 'load', et_init);

You can see the finished code in action here.

Conclusion

In this article, we have seen two ways in which well structured markup can be paired up with Javascript and the W3C DOM to achieve a useful effect. I hope this article has inspired you to seek new ways of using Javascript and intelligent markup.

Further Reading

There are many other excellent examples of Javascript effects based on structural markup. Here are just a few that are worth checking out:

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.