Dochula Pass, Bhutan

This post was updated. See bottom of post for details.

I developed a set of JavaScript routines (W3C DOM standard) for hiding and revealing information on a page that you should be able to plug in to a wide range of content. Please feel free to use the code (though an acknowledgement would be nice.)

Files
JavaScript: expandcollapse.js

CSS: expandcollapsestyle.css
Original HTML: news.html
Resulting HTML: newsWithJS.html

The uncollapsed text. (Click to see larger version.)

We’ll illustrate how to apply this with an example. The picture shows what it looks like initially. (View the HTML.) We’ll collapse the additional news after the first item to just the headlines, but allow you to reveal the detail by clicking or tabbing and hitting return.

[I applied this to a hacked down version of someone else’s page, because I was short on time. This is good, in that it shows that it’s easy to apply this to existing pages. However, due to my hacking, the general markup of the page may look a little strange in parts. Please ignore that.]

Structuring the content

The markup of content you want to hide and reveal may be structured in a number of ways. This approach assumes that:

  1. you will click on a block element (which we will call the trigger) to cause some content below it to expand or contract
  2. the content revealed/hidden by clicking on the trigger can be in any number of block elements of any type. (You can also include other block elements above the trigger, if you like, though they won’t be hidden.)
  3. each trigger and its revealable content is bounded by a block element. (We will use a div, but it could be any block element.)
  4. all the expanding and collapsing content is surrounded by another element with an id. This allows you to work with expanding content in different areas on the same page separately. (Again we use a div, with the id otherNews, but the id could just as easily be on the body element, since we only have one area of affected content on this page.)

The diagram below shows the arrangement used in the example file. The trigger element is red. The content to be hidden/revealed is green. You don’t have to use an h3 as the trigger. You could even use an ordinary paragraph tag. If you do, however, you should use a class name on each trigger element, so that the trigger can be identified.

Note that the trigger element should not contain an <a> element, since the JavaScript will add an <a> element to create a clickable zone. (It doesn’t make sense, anyway.)

The structure of the content in the example.

Setting up the markup

Very little change is required to the markup.

What I did

Add this to the document head:
<script type="text/javascript" src="expandcollapse.js"></script>
<link rel="stylesheet" type="text/css" href="expandcollapsestyle.css"/>

Add this to the body element start tag:
onload="setCollapseExpand('otherNews', 'h3','');revealControl('On'); revealControl('Off');"

Add this next to the RSS icon, just above the expanding content:
<a id="On" name="On" onclick="openAll('otherNews', 'h3','');" href="#_" class="hideIfNoJS">Open All</a>
<a id="Off" name="Off" onclick="closeAll('otherNews', 'h3','');" href="#_" class="hideIfNoJS">Close All</a>

Notes:

  1. onload="setCollapseExpand('otherNews', 'h3','');"

    After the document has loaded, this collapses the content.

    The JavaScript will look through the div with id otherNews for all h3 elements. It then finds the parent of the h3 element, and adds a class name to all the remaining elements after the h3 within that parent (a div, in our case). The class is associated with styling that makes these elements disappear. It will also surround the contents of the h3 element with an a element. This allows keyboard users to access the functionality using tabs. Each a element is given an onclick function to enable it to toggle the hidden content on or off.

    If we had wanted to use an ordinary p tag with a class name of, say, trigger rather than the h3, the onload code would look like this:

    onload="setCollapseExpand('otherNews', 'p','trigger');"
  2. Optional. You may want to add some buttons to expand and collapse all text in one go. If so you’ll need to add these to the markup. In our example I added the following code alongside the RSS feed icon. I used an a element so that keyboard users can tab to it.

    <a id="On" name="On" onclick="openAll('otherNews', 'h3','');" 
       href="#_" class="hideIfNoJS">Open All</a>
    <a id="Off" name="Off" onclick="closeAll('otherNews', 'h3','');" 
       href="#_" class="hideIfNoJS">Close All</a>
    

    I added the class name hideIfNoJS to each a element. We can now use CSS to hide this text unless JavaScript is detected.

    We then need to add two more statements to the onload value on the body tag, one for each a element.

    revealControl('On'); revealControl('Off');

    After the document loads, the JavaScript will remove those class names, and the switches will become visible.

  3. <link rel="stylesheet" type="text/css" href="expandcollapsestyle.css"/>

    CSS will drive most of the behaviour. The JavaScript simply changes the class names associated with the markup. This references a stylesheet that will do all the hard lifting.

A walk through the CSS

Let’s take a look at the CSS in the expandcollapsestyle.css file.

First, we add some styling to the new ‘Open All’ and ‘Close All’ text we added. This will make this text look like small graphical buttons, and change the cursor to a pointing hand as we mouse over them.

   a#On, a#Off {
      padding: 0.1em 0.5em 0.1em 0.5em;
      margin: 0 0.5em 0 0;
      text-decoration: none;
      background: #005a9c;
      color: #fc6;
      font-weight: bold;
      cursor: pointer;
      }

Next, we add a rule to remove the ‘Open All’ and ‘Close All’ buttons from view initially. The revealControl calls in the body onload attribute will remove this class if JavaScript is enabled.

   .hideIfNoJS {
      display: none;
      }

Now, we style the trigger text (in our case the h3 elements).

The first set of rules makes the cursor become a pointer when we mouse over the text, and adds a graphic to show whether the content is revealed or not.

   .triggerOpen {
	background:url(http://www.w3.org/International/icons/open-thin.gif)
              no-repeat left 2px #fffaf0;
	}

   .triggerClosed {
	background:url(http://www.w3.org/International/icons/close-thin.gif)
              no-repeat left 2px #fffaf0;
	}

You can, of course, change the styling to suit yourself. For example, you may want to use a different graphic.

We also fix the colour of the trigger text, pads the left side of the text so that you can see the graphic, and make the trigger change colour as you mouse over it. (Note that the JavaScript has introduced this a element.)

.triggerOpen a, .triggerClosed a {
	padding-left:14px;
	color:#000;
	text-decoration: none;
	cursor: pointer;
	}

.triggerOpen a:hover, .triggerClosed a:hover {
	color:#00f;
	}

Finally, we add the styling for the content that will be hidden/revealed. The .hiddenContent class will be attached to content by the JavaScript to hide it.

   .hiddenContent {
      display: none;
      }

When that content is not hidden, it gets the revealedContent class. We added some styling to pad the left side of the blocks by the same amount as the trigger text.

   #otherNews .revealedContent {
      padding-left: 14px;
      }
   #otherNews ul.revealedContent {
      padding-left: 30px;
      margin-left: 0;
      }

The end result

The collapsed text. (Click to see larger version.)

This picture shows what you will see when you open the page in a user agent that has JavaScript turned on. (See the HTML.) If JavaScript is turned off, you will see exactly what you saw before.


Updates to this post

2007-07-01: Moved cursor:pointer from rules for .triggerOpen and .triggerClosed to the rules for ‘.triggerOpen a, .triggerClosed a’. Stops the pointer appearing to the right of the trigger text. Also added note about <a> in trigger.

2007-06-05: Small change to CSS to ensure that the expand/collapse works when clicking on the + or – icon too. (Moved the padding.)

2007-04-18: Largely rewrote the text to make it more readable, and to take into account changes made to the JavaScript and CSS files (which incorporate the ideas from several comments below).

View blog reactions

(I’m making notes here so I can find these techniques again later.)

I wanted to use JavaScript (W3C DOM compliant) to wrap the content of a heading with an a element, ie.

<h3>This is <em>my</em> header</h3>

Needed to become

<h3><a href=”#mytarget”>This is <em>my</em> header</a></h3>

Here’s what I came up with:

var h = document.getElementBySomeMethod('h3'); // grab the heading
var a = document.createElement('a');       // create an a element
    a.setAttribute('href', '#mytarget');   // set the href
while (h.childNodes.length > 0) {          // for each child node in the h3
    a.appendChild( content.firstChild );   // move the node to the a element
    }
h.appendChild(anchor);                    // stick a under the now empty h3

It seems so simple now to look at. Took me ages to figure it out. 🙁

You should always use the lang and/or xml:lang attributes in HTML or XHTML to identify the human language of the content so that applications such as voice browsers, style sheets, and the like can process that text. (See Declaring Language in XHTML and HTML for the details.)

You can override that language setting for a part of the document that is in a different language, eg. some French quotation in an English document, by using the same attribute(s) around the relevant bit of text.

Suppose you have some text that is not in any language, such as type samples, part numbers, perhaps program code. How would you say that this was no language in particular?

There are a number of possible approaches:

  1. A few years ago we introduced into the XML spec the idea that xml:lang=”” conveys that ‘there is no language information available’. (See 2.12 Language Identification)

  2. An alternative is to use the value ‘und’, for ‘undetermined’.

  3. In the IANA Subtag Registry there is another tag, ‘zxx’, that means ‘No linguistic content’. Perhaps this is a better choice. It has my vote at the moment.

xml:lang=”” Is ‘no language information available’ suitable to express ‘this is not a language’? My feeling is not.

If it were appropriate, there are some other questions to be answered here. With HTML an empty string value for the lang or xml:lang attribute produces a validation error.

It seems to me that the validator should not produce an error for xml:lang=””. It needs to be fixed.

I’m not clear whether the HTML DTD supports an empty string value for lang. If so, the presumably the validator needs to be fixed. If not, then this is not a viable option, since you’d really want both lang and xml:lang to have the same values.

und Would the description ‘undetermined’ fit this case, given that it is not a language at all? Again, it doesn’t seem right to me, since ‘undetermined’ seems to suggest that it is a language of some sort, but we’re not sure which.

zxx This seems to be the right choice for me. It would produce no validation issues. The only issue is perhaps that it’s not terrible memorable.

This is an attempt to summarise and move forward some ideas in a thread on www-international@w3.org by Christophe Strobbe, Martin Duerst, Bjoern Hoermann and Tex Texin. I am sending this to that list once more.

I use XMetal 4.6 for all my XHTML and XML authoring. As someone who has been advocating for some time that you should always declare the human language of your content when creating Web content, I’m finding XMetal’s spell checker both exciting and frustrating. Here are a few tips that might help others.

The exciting part is that XMetal figures out which spell checker to use based on the xml:lang language declarations. Given the following code:

<html xml:lang="en-us" lang="en-us" ... > 
...
<p>behavior localization color</p>
<p>behaviour localisation colour</p>
<p xml:lang="fr" lang="fr">ceci est français</p>
<p lang="gr" xml:lang="gr">Κάνοντας τον Παγκόσμιο Ιστό πραγματικά Παγκόσμιο</p>
...

The spell checker will recognize three errors (behaviour localisation colour). The en-us value in the html tag causes it to use the US-English spell check dictionary, and the fr and gr values in the last two paragraphs cause it to use a French and Greek dictionary, respectively, for the words in those elements. Great!

Picture of the spell checker in action.

Note that, since XMetal is an XML editor, rather than an HTML editor, it is the value in the xml:lang attribute rather than the one in the lang attribute that counts here. For XHTML 1.0 content served as text/html, of course, you should use both.

The following, however, are things you need to watch out for:

  1. If your html tag contains just xml:lang=”en” your spell checking won’t be terribly effective, since all the English dictionaries (US, UK, Australia, and Canada) will be used. This means that for the code above you will receive no error notifications, since each spelling occurs in at least one dictionary.

    This is logical enough, though it’s something you may not think about when spell checking. (Even if you go into the spell checker options and set, say, the US English spell checker, the language declaration will override that manual choice.)

  2. If you want to write British English, you would normally put en-GB in the xml:lang (because that’s what BCP 47 says you should do). Unfortunately this will produce no errors with our test case above! XMetal doesn’t recognise the GB subtag, and reverts to xml:lang=”en”. To get the behaviour you are expecting you have to put en-UK in xml:lang. This is really bad. It means you are marking up your content incorrectly. Presumably the same holds true for other languages. I see CF for Canadian French, rather than CA, SD for Swiss German rather than CH, etc.

It’s good to see that the language markup is being used for spell-checking. However, it’s a case of two steps forward, one step back. Which is a shame.

UPDATE: Justsystems have worked on this some more. See my later blog post for details.