this week’s project is tabbing content on a web page.  it should be simple to implement, with as little intrusion into the HTML structure of the page and doing as much work automatically as possible. it should degrade cleanly, so that the entire content is presented normally if javascript is not enabled, and content should also present normally if the page is printed instead of viewed on-screen.

we’ll be using the prototype and script.aculo.us javascript libraries to do most of the heavy lifting. prototype provides all the DOM selection and introspection tools we’ll need, and better array management. builder, which is part of script.aculo.us, provides tools for creating new DOM nodes, which we’ll use to build our tab navigation and insert it into the document.

first, our tabbing code needs to look for any blocks of tabbed content within the web page. we’ll define these by enclosing them within a <div> block, with an appropriate marker class. however, it can’t do this until the web browser has loaded the document and parsed the HTML markup, so we use prototype to observe that event, and invoke our initialization function when it happens. if there are any tabbed blocks, then…

for each tabbed block, do a bit of setup, then get a list of the tab blocks within it. again, we’ll define these within <div> blocks, with appropriate marker classes. each tab <div> should also have a title, which will be used for the tab link.

for each tab, attach a unique identifier to the content, build a new link to activate it, and then hide it by giving it the ‘inactive‘ class. after we’ve built our list of tab links (tab_nav), insert it into the top of the tabbed block. finally, determine the first tab in the current tabbed block, and invoke lift_tab() to activate it.

// tabbed.js

document.observe('dom:loaded',init_tabs);

function init_tabs () {
  if (blocks = $$('div.tab_block')) {
    var B = {}; Builder.dump(B);   // for convenience

    blocks.each(function (block) {
      block_attr = { 'class': 'tab_nav' };
      items = [];

      if (tabs = block.select('div.tab')) {
        tabs.each(function (tab) {
          tbd_id = tab.identify();
          a_attr = { 'onclick': "lift_tab(this,'" + tbd_id + "')" };

          items.push(B.LI({},B.A(a_attr,tab.title)));
          tab.addClassName('inactive');
        });
        block.insert({ 'top': B.UL(block_attr,items) });
        lift_tab(block.down('ul').down('a'), tabs[0].identify());
      }
    });
  }
}

the lift_tab() function expects a tab link and content identifier. it deactivates all the tab links within the same list, then activates the specified tab link. in a similar fashion, it activates the corresponding content. note, however, the opposite sense of the classes involved. tab links are explicitly active, content is explicitly not inactive. this is done this way to make the CSS style sheets a little simpler.

// tabbed.js continued...

function lift_tab (link, content_id) {
  link.up('ul').select('a').each(function (a) {
    a.removeClassName('active');
  });
  link.addClassName('active');

  link.up('div').select('div.tab').each(function (d) {
    d.addClassName('inactive');
  });
  $(content_id).removeClassName('inactive');
}

next, we define the CSS style sheets which actually effect the changes which the javascript is making to the HTML elements. on-screen, we want our list of tabs to be displayed inline, in a single row, with appropriate margins and borders. inactive tab links are greyed out, whereas active tab links are given a white background and bottom border to mask the border which normally separates the navigation and content, visually linking that tab to the content. finally, inactive content is hidden by setting its display to ‘none’.

// tabbed-screen.css

ul.tab_nav { margin: 0px; padding: 2px 0px; border-bottom: 1px solid; }
ul.tab_nav li { display: inline; margin: 0px; padding: 0px;
  list-style: none; }
ul.tab_nav li a { margin: 0px; margin-left: 3px; padding: 3px 0.5em;
  border: 1px solid; border-bottom: none;
  color: #808080; font-weight: bold; text-decoration: none; }
ul.tab_nav li a.active { border-bottom: 1px solid #FFFFFF;
  background: #FFFFFF; color: #000000; }
div.inactive { display: none; }

styling for print is much simpler. hide the navigation, and all tab blocks are presented normally.

// tabbed-print.css

.tab_nav { display: none; }

finally, we include all our javascript libraries and style sheets into the page. since the code is contingent upon having tabbing <div> blocks defined in the HTML markup, we can simply add this to a page template. this makes it available wherever we want to use it, and it simply does nothing wherever we don’t.

<script type="text/javascript" src="/lib/prototype.js"></script>
<script type="text/javascript" src="/lib/builder.js"></script>
<script type="text/javascript" src="/lib/tabbed.js"></script>

<link rel="stylesheet" type="text/css" media="screen"
    href="/lib/tabbed-screen.css" />
<link rel="stylesheet" type="text/css" media="print"
    href="/lib/tabbed-print.css" />

to turn a block of content into tabbed content,
enclose the entire block in <div class="tab_block"> ... </div>
and each tabbed block in <div class="tab" title="Title"> ... </div>

<h1>tabbing content with prototype</h1>
<p>this paragraph is above the tabbed content.</p>

<div class="tab_block">
  <div class="tab" title="Overview">
    <p>an overview of what we're doing and how it works.</p>
    ...
  </div>
  <div class="tab" title="Javascript">
    <p>details on the javascript coding which makes this work.</p>
    ...
  </div>
  <div class="tab" title="CSS">
    <p>details on the CSS styling which makes this work.</p>
    ...
  </div>
  <div class="tab" title="Implementation">
    <p>finally, how to put everything together.</p>
    ...
  </div>
</div>

<p>this paragraph is below the tabbed content.</p>