Posts Tagged ‘unobtrusive’

An unobtrusive badge for Google Reader’s shared items

Wednesday, May 21st, 2008

I am a user of Google Reader to get through the vast amounts of RSS feeds I subscribed to. I think it is safe to say that reading RSS and twittering has replaced most of my web surfing.

Like most big RSS readers, Google reader also allows you to share great finds you had with people who want to and are in your social neighbourhood. You can either get these finds as a feed or as a little badge (called a clip in Google lingo) to include in your blog or other sites.

The out-of-the-box version of this badge can be customized and results in two JavaScript includes which write out the badge.

That is nice, but I don’t quite care for things that could offer functionality without JavaScript but don’t bother, which is why I checked more closely what the Google badge does.

If you look at the generated script includes you’ll find for example the following URL ( added spaces to avoid breaking my blog :) )

http://www.google.com/ reader/public/javascript/ user/07479231772993841072/ state/com.google/broadcast? n=5&callback=GRC_p%28%7Bc%3A%22green%22%2Ct %3A%22Christian%20Heilmann%27s %20shared%20items%22%2Cs%3A%22false%22%7D%29%3Bnew%20GRC

Clicking this will get you a JSON object with a wrapper function (and for some reason a comment that this is a JavaScript file), which means you can use this for your own purposes.

All you need is your user ID, which you can get this one easily from your shared items homepage that Google Reader offers. In my case this is http://www.google.com/reader/shared/07479231772993841072.

The other interesting parameters of the JSON API are the n parameter defining the amount of items and the callback parameter defining the name of the function call wrapped around the JSON data.

Putting all of this together it was easy to create a badge that uses the following HTML to show off my shared items on Google Reader.

<div id="greader-shared-items" class="items5">
<p>
<a href="http://www.google.com/reader/shared/07479231772993841072">My Shared Items on Google Reader</a>
</p>
</div>
<script src="google-reader-badge-v1.0.js"></script>

Visitors without JavaScript will still be able to click through to the page of my shared items. Those with JavaScript will get the latest five.

You can see the badge in action and download it for yourself on the demo page (using tutorialbuilder):

Hacking SlideShare’s embed adding a preview and be a lot shorter and readable

Thursday, April 17th, 2008

Edit: There is a bug in the script (see comments) but somehow Googlecode does not allow me to edit my own file. I will fix it once I got around that issue.The bug reported in the comments is now fixed, sadly enough I also had to re-write the converter as Google code does not allow me to replace an older version of a download (or is there a trick?). The new file is called previewer2.js

As readers of this blog know, I am a big fan of SlideShare as a distribution platform for my presentation slides. However, there are some things that annoy me about it.

One of them is the rather verbose embed code SlideShare offers you:


<div style="width:425px;text-align:left" id="__ss_335941">
<object style="margin:0px" width="425" height="355">
<param name="movie" value="http://static.slideshare.net/swf/ssplayer2.swf?doc=highlandflingbadgesnew-1207296687342384-9"/>
<param name="allowFullScreen" value="true"/>
<param name="allowScriptAccess" value="always"/>
<embed src="http://static.slideshare.net/swf/ssplayer2.swf?doc=highlandflingbadgesnew-1207296687342384-9" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"></embed>
</object>
<div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"><a href="http://www.slideshare.net/?src=embed"><img src="http://static.slideshare.net/swf/logo_embd.png" style="border:0px none;margin-bottom:-5px" alt="SlideShare"/></a>
| <a href="http://www.slideshare.net/cheilmann/building-badges-for-distribution-335941?src=embed" title="View 'Building Badges for distribution' on SlideShare">View</a> |
<a href="http://www.slideshare.net/upload?src=embed">Upload your own</a>
</div>
</div>

That is quite a mouthful and the main issue is that when you use several slide embeds in one document, you’ll slow down the rendering of your page as each of these Flash embeds need to be instantiated and tries to pre-cache the first three slides from S3.

I’ve analyzed the code a bit, added some other info I found in the RSS feed and came up with a small JavaScript that embeds slides in a different way. All you need there is the following code:


<div class="slideshare">
<a href="http://www.slideshare.net/cheilmann/building-badges-for-distribution-335941#highlandflingbadgesnew-1207296687342384-9">View 'Building Badges for distribution' on SlideShare</a>
<script src="http://chrisslidesharehacks.googlecode.com/files/previewer2.js"></script>
</div>

This gives slideshare the same SEO link love but is a lot less to add. Instead of the full slide include, you’ll get a preview image you can click that gets replaced with the flash movie. The following are examples:

Now, in order to convert one to the other you could do it by hand, or use the slideshare embed converter or install the Greasemonkey script

So far this is a hack, but I talked to Jonathan Boutelle about it yesterday night at the San Francisco JavaScript meetup and he is happy to pursue this idea further. My wishlist:

  • A larger preview image
  • A rest API call that gives me this information in a readable manner

Example of an unobtrusive, lazy-loading badge using the Twitter API

Friday, April 11th, 2008

Following questions I had about my talk at Highland Fling about badges for distribution and a twitter nagging by Tantek about the official twitter badge I thought I’d have a go at creating a twitter badge following some of the best practices I mentioned in my talk. Here’s the result.

The markup

Instead of HTML containers that will be seeded with real data when JavaScript is available and pointless when it isn’t, I wanted to build on top of HTML that makes sense without scripting and get all the info my script needs from there.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 1//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Example of a unobtrusive, lazy loading twitterbadge</title>
</head>
<body>
<div id="twitterbadge">
<h2><a href="http://twitter.com/codepo8">My twitter updates</a></h2>
</div>
<script src="twitterbadgev2.js" type="text/javascript"></script>
</body>
</html>

In order to customise the badge, I allow for CSS classes with information to be added to the main container:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 1//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Example of a unobtrusive, lazy loading twitterbadge</title>
</head>
<body>
<div id="twitterbadge" class="amount-10 skin-blue userinfo">
<h2><a href="http://twitter.com/codepo8">My twitter updates</a></h2>
</div>
<script src="twitterbadgev2.js" type="text/javascript"></script>
</body>
</html>

They mean the following:

  • amount-n defines the amount of tweets to be displayed with n being an integer
  • skin-name defines the skin you want to use (for now this is grey and blue)
  • userinfo defines if the user’s avatar, name and location should be displayed.

The script

Here’s the full script and we’ll go through the bits one by one.

twitterbadge = function(){
var config = {
countDefault:5,
badgeID:'twitterbadge',
userID:'twitterbadgeuser',
tweetsID:'twitterbadgetweets',
userinfo:'userinfo',
stylesmatch:/skin-(w+)/,
amountmatch:/amount-(d+)/,
styles:{
'grey':'twitterbadge.css',
'blue':'twitterbadgeblue.css'
}
};
var badge = document.getElementById(config.badgeID);
if(badge){
var link = badge.getElementsByTagName('a')[0];
if(link){
var classdata = badge.className;
var head = document.getElementsByTagName('head')[0];
var amount = config.amountmatch.exec(classdata);
var amount = amount ? amount[1] : config.countDefault;
var skin = config.stylesmatch.exec(classdata);
if(skin && skin[1]){
var style = document.createElement('link');
style.setAttribute('rel','stylesheet');
style.setAttribute('type','text/css');
style.setAttribute('href',config.styles[skin[1]]);
head.insertBefore(style,head.firstChild);
}
var name = link.href.split('/');
var resturl = 'http://twitter.com/statuses/user_timeline/' +
name[name.length-1] + '.json?callback=' +
'twitterbadge.show&count=' + amount;
var script = document.createElement('script');
script.src = resturl;
script.type = 'text/javascript';
function show(result){
if(classdata.indexOf(config.userinfo) != -1){
var user = document.createElement('p');
user.id = config.userID;
var img = document.createElement('img');
img.src = result[0].user.profile_image_url;
img.alt = result[0].user.name;
user.appendChild(img);
var ul = document.createElement('ul');
var data = ['screen_name','name','location'];
for(var i=0;data[i];i++){
if(result[0].user[data[i]]){
var li = document.createElement('li');
li.appendChild(document.createTextNode(result[0].user[data[i]]));
ul.appendChild(li);
}
}
user.appendChild(ul);
badge.appendChild(user);
}
var tweets = document.createElement('ul');
tweets.id = config.tweetsID;
for(var i=0,j=result.length;i<j;i++){
var username = result[i].user.screen_name;
var li = document.createElement('li');
var span = document.createElement('span');
span.innerHTML = result[i].text+' ';
li.appendChild(span);
var link = document.createElement('a');
link.setAttribute('href','http://twitter.com/' + username +
'/statuses/'+result[i].id);
link.appendChild(document.createTextNode(relative_time(result[i].created_at)));
li.appendChild(link);
tweets.appendChild(li);
}
badge.appendChild(tweets);
}
function relative_time(time_value) {
var values = time_value.split(" ");
time_value = values[1] + " " + values[2] + ", " + values[5] + " " + values[3];
var parsed_date = Date.parse(time_value);
var relative_to = (arguments.length > 1) ? arguments[1] : new Date();
var delta = parseInt((relative_to.getTime() - parsed_date) / 1000);
delta = delta + (relative_to.getTimezoneOffset() * 60);
if (delta < 60) {
return 'less than a minute ago';
} else if(delta < 120) {
return 'about a minute ago';
} else if(delta < (60*60)) {
return (parseInt(delta / 60)).toString() + ' minutes ago';
} else if(delta < (120*60)) {
return 'about an hour ago';
} else if(delta < (24*60*60)) {
return 'about ' + (parseInt(delta / 3600)).toString() + ' hours ago';
} else if(delta < (48*60*60)) {
return '1 day ago';
} else {
return (parseInt(delta / 86400)).toString() + ' days ago';
}
}
}
}
return {
show:show,
init:function(){
head.appendChild(script);
}
};
}();
twitterbadge.init();

I am using the revealing module pattern to keep code short and avoid global callback methods. However, there is a slight Opera oddity with generated script nodes in module patterns so we have to deviate from the norm there with an extra init() method call after the main module.

The first thing I thought of providing is a configuration object for the script. This makes it easy to change settings of it without having to hunt through the whole script and is just a nice service for the implementer:


twitterbadge = function(){
var config = {
countDefault:5,
badgeID:'twitterbadge',
userID:'twitterbadgeuser',
tweetsID:'twitterbadgetweets',
stylesmatch:/skin-(w+)/,
amountmatch:/amount-(d+)/,
styles:{
'grey':'twitterbadge.css',
'blue':'twitterbadgeblue.css'
}
};

Here we have all the IDs in use, the style names and the corresponding file names and the regular expressions to get the data from the CSS class name. All of the IDs and classes are hooks to define your own skins. There is also a countDefault variable to define how many items should be shown when the amount class is not set.


var badge = document.getElementById(config.badgeID);
if(badge){
var link = badge.getElementsByTagName('a')[0];
if(link){

I test for the badge and that it contains a link as this is where we will get all our configuration data from.


var classdata = badge.className;
var head = document.getElementsByTagName('head')[0];
var amount = config.amountmatch.exec(classdata);
var amount = amount ? amount[1] : config.countDefault;
var skin = config.stylesmatch.exec(classdata);
if(skin && skin[1]){
var style = document.createElement('link');
style.setAttribute('rel','stylesheet');
style.setAttribute('type','text/css');
style.setAttribute('href',config.styles[skin[1]]);
head.insertBefore(style,head.firstChild);
}

Then I am ready to read the information from the class. I set a shortcut to the document head and read the amount of tweets to be displayed. If there is no amount-n class set I fall back to the default.

Next is the skin, I check if the class was set and if that is the case I create a new link element pointing to the right skin. I get the href from the configuration styles object.

Notice that I use insertBefore() to add the style to the head of the document and not appendChild(). This ensures to a degree that the skin css file will not override settings that might be in other stylesheets. The last included style sheet rules them all.


var name = link.href.split('/');
var resturl = 'http://twitter.com/statuses/user_timeline/' +
name[name.length-1] + '.json?callback=' +
'twitterbadge.show&count=' + amount;
var script = document.createElement('script');
script.src = resturl;
script.type = 'text/javascript';

Now it is time to find the user name (by splitting the href attribute of the link) and assemble the REST url to get the twitter data. Normally I would have added the new script node to the head directly aftwerwards, but Opera doesn’t like this.


function show(result){
if(classdata.indexOf(config.userinfo) != -1){
var user = document.createElement('p');
user.id = config.userID;
var img = document.createElement('img');
img.src = result[0].user.profile_image_url;
img.alt = result[0].user.name;
user.appendChild(img);
var ul = document.createElement('ul');
var data = ['screen_name','name','location'];
for(var i=0;data[i];i++){
if(result[0].user[data[i]]){
var li = document.createElement('li');
li.appendChild(document.createTextNode(result[0].user[data[i]]));
ul.appendChild(li);
}
}
user.appendChild(ul);
badge.appendChild(user);
}

Now it is time to start the core functionality: the show method that will be invoked by the twitter REST API callback. I check if the userinfo has been set and create the markup accordingly. Nothing amazing here.


var tweets = document.createElement('ul');
tweets.id = config.tweetsID;
for(var i=0,j=result.length;i<j;i++){
var username = result[i].user.screen_name;
var li = document.createElement('li');
var span = document.createElement('span');
span.innerHTML = result[i].text+' ';
li.appendChild(span);
var link = document.createElement('a');
link.setAttribute('href','http://twitter.com/' + username +
'/statuses/'+result[i].id);
link.appendChild(document.createTextNode(relative_time(result[i].created_at)));
li.appendChild(link);
tweets.appendChild(li);
}
badge.appendChild(tweets);
}

Next I get the tweets information, assemble a list and add it to the badge.


function relative_time(time_value) {
[...]
}
}
}
return {
show:show,
init:function(){
head.appendChild(script);
}
};
}();
twitterbadge.init();

The relative_time method is actually taken from the original twitter badge and calculates how old the tweets are. I end the module with a return statement that defines the public methods (in this case only show) and add the script node to call the REST API in an init method. This is only necessary to fix the Opera issue.

Download and Example

You can download the twitter badge and see it in action.

[tags]unobtrusive,badge,twitter,api,json,thehighlandfling,highlandfling08,highlandfling,thehighlandfling08[/tags]

An attempt for a more accessible edit-in-place solution

Friday, January 4th, 2008

Today I will try to find a more accessible way to provide an edit-in-place script. The solution described here is probably not ready for real life yet, please test it in different environments and provide fixes. It is licenced with creative commons share-alike, so go nuts!

I really like the idea of edit-in-place. Probably the most used example of edit-in-place is flickr which allows you to click any heading or description when you are logged in to directly edit it. The reason I like edit-in-place is that it makes it a lot easier for users to describe things, which hopefully results in more accessible and easier to find web sites (after all text is what gets indexed and what is available to everybody). The drawback of edit-in-place is that a lot of solutions are not very accessible. They add a click handler to elements that are not necessarily available to assistive technology and are not at all accessible with a keyboard.

A non-scripting solution attempt

Technically the easiest solution to create an accessible edit-in-place would be to use input fields with labels and use CSS to make them look like the other elements. The code could be the following:

<h1 class="editable">
  <label for="mainheading">Heading:</label>
  <input type="text" id="mainheading" 
         value="Otters in eastern Europe">
</h1>

And the CSS:

.editable label{
  position:absolute;
  top:0;
  left:-9999px;
}
.editable input{
  border:none;
  font-family:arial,sans-serif;
}

But alas! Is it more accessible? I am not sure, as the semantic goodness of a heading is disturbed. I am quite sure search engines will frown upon it and as screen readers work differently in forms mode than in reading mode it might even be more confusing. So let’s scratch that.

Making it work unobtrusively

The next step would be to use a normal heading and somehow connect it with a form field somewhere else in the document. The cool thing about this is that we have something like that in plain HTML: targeted links. For example:

<form id="editform" action="servermagic.php">
<h1 class="editable">
  <a href="#editheader">Edit me, wuss!</a>
</h1>
<p class="editable">
  <a href="#editdescription">Me as well, do it!</a>
</p>
<div id="editingsection">
  <p>
    <label for="editheader">Content of main heading:</label>
    <input type="text" id="editheader" name="editheader">
  </p>
  <p>
    <label for="editdescription">Content of description:</label>
    <input type="text" 
           id="editdescription" 
           name="editdescription">
  </p>
  <p><input type="submit" value="Save Changes"></p>
</div>
</form>

This works without JavaScript (but somehow my Firefox does not highlight the form field when you hit the link, does anyone know why? Please comment!). All it needs to turn it into a working version of edit-in-place is a JavaScript. What the script does is:

  • find the section with the ID “editsection” and hide it from view
  • find all elements with the class “editable” and add a click handler pointing to an edit function
  • override the submit event of the form to point to a store function

The edit function should do the following:

  • check if another element is already being edited and focus that if there is one
  • Get the ID of the form element from the href of the targeted link
  • set the value of the form element to the content of the element
  • set an “edited” style to the original element to hide it
  • show the form field where the original link was
  • focus the form field
  • tell the main script what element is currently being edited

The store function should:

  • check if there is an element edited (to avoid overriding normal form submission)
  • set the content of the targeted link to the value of the field
  • move the form field back to where it came from
  • set the focus back to the link
  • store the content asynchronously (not implemented here)
  • stop the normal form submission
  • reset the edit state of the script to none

Following is the script that does exactly that. I am using the YUI in this example as I like to concentrate on writing scripts rather than worrying about browser problems. That said, notice that you need to wrap form field focus in a timeout in Firefox, what’s with that?

YAHOO.namespace('ch');
YAHOO.ch.editinplace = function(){
 
  /* Names and IDs used */
  var namesandids = {
    editsection:'editingsection',
    edited:'edited',
    hidden:'hidden',
    editable:'editable',
    form:'editform'
  };
 
  var YE = YAHOO.util.Event,YD = YAHOO.util.Dom;
 
  var edit = {};
  var editingsection = YD.get(namesandids.editsection);
  if(editingsection){
    YD.addClass(editingsection,namesandids.hidden);
 
    function doedit(e){
      if(!edit.target){
        var t = YE.getTarget(e);
        if(t.href){
          var fieldid = t.getAttribute('href').split('#')[1];
          var field = YD.get(fieldid);
          this.appendChild(field);
          field.value = t.innerHTML;
          YD.addClass(t,namesandids.edited);
          setTimeout(function(){field.focus();},10)
          edit = {target:t,field:field,id:fieldid};
        };
      } else {
        setTimeout(function(){edit.field.focus();},10)
      }
      YE.preventDefault(e);
    };
 
    function store(e){
      if(edit.target){
        edit.target.innerHTML = edit.field.value;
        YD.removeClass(edit.target,namesandids.edited);
        editingsection.appendChild(edit.field);
        edit.target.focus();
        // Ajax Magic here, you can used edit.id as the id 
        YE.preventDefault(e);
        edit = {};
      };
    };
 
    var edits = YD.getElementsByClassName(namesandids.editable);
    YE.on(edits,'click',doedit);
    YE.on(YD.get(namesandids.form),'submit',store);
 
  }
}();

Try out the solution and Download the example page with the script as a zip and tell me what you think!

Tested in Firefox, IE7 and Opera 9 on PC, please get me some more feedback on other systems and browsers.

[tags]unobtrusive,accessibility,editinplace,interface[/tags]

Transcript of the Paris Web 2007 workshop on Unobtrusive JavaScript

Friday, November 23rd, 2007

This is a step-by-step description accompanied by code examples of the “Unobtrusive JavaScript” workshop at Paris Web 2007 in Paris, France.

You can download all the files used in this here: parisweb2007_workshop.zip

At the start of the workshop I gave two promises:

  • Participants who know JavaScript will find out that they do often write far too much code
  • Participants who don’t know JavaScript will learn how they can prepare and help JS developers building unobtrusive products.

First Step: Analyzing the task and the HTML

We started with an HTML template (01_template.html) and analyzing it. We pointed out that the template is not optimal as it contains embedded CSS which in a live product should be in a separate CSS file. We also pointed out that it is OK to start like this, as it keeps maintenance easier.

The template is a list of links (a table of contents) pointing to a group of headings further down the page. The idea is that visitors can click the table of contents(TOC) and move down the document. “Back to top” links allow them to go back up again. The TOC has an ID of “toc”, the back links a class of “back”.

We have a block of CSS in the head of the document and a script block at the end of the body. Having a script block at the end of the body makes sure that all the HTML we want to reach in our JavaScript is available when it gets executed.

The task we wanted to achieve in the workshop was to turn this template into a dynamic interface that hides all the content and only shows the one connected to the link in the TOC when a visitor clicks it.

Second Step: Knowing browser issues and using them to our advantage

The problem we had was to know which section to show and hide when the links are clicked. The logical connection was easy: every link inside the TOC has an href attribute that points to an ID in the heading it connects to. We can use this in our script. The problem was that we have no idea about the content that follows the heading. We needed a way to make sure we can group all the elements we need to hide into an element.

The thing that helped us there is a lesser known Internet Explorer bug if you use your keyboard to tab through links pointing to anchors inside the page the page does jump there, but the keyboard focus does not move with it. The easiest workaround is to nest the anchor element inside an element that hasLayout. In our case we added DIV elements around each heading and content section and set their widths to 100% (02_iefix.html).

Third Step: The script

We had all the HTML we needed and the page already did what we wanted to. Now we started thinking about how to override the default behaviour and make it work for us. The main changes we wanted to do was:

  • Hide all the content sections
  • Hide the back links as we don’t need them when we show only one content section
  • Show the content section connected with the link in the TOC when it is clicked

We discussed the different possibilities to do that (looping through the DOM, getting all child elements of the DIV and set their style.display value to ‘none’ and so on). I then proposed that the easiest way by far is to write a simple JavaScript that adds a class to the body of the document when JS is available and define CSS that hides the necessary elements. That way we can easily let CSS do all the heavy DOM traversing and we offer designers a hook to style the non-JavaScript and the JavaScript version differently.

The JavaScript:


document.body.className&nbsp;=&nbsp;'js';

The CSS:


/*&nbsp;Scripting&nbsp;dependent&nbsp;styles&nbsp;*/

body.js&nbsp;#toc{
float:left;
width:20%;
margin-right:5%;
}
body.js&nbsp;div{
float:right;
width:70%;
}
body.js&nbsp;div,body.js&nbsp;.back{
position:absolute;
left:-9999px;
}

We then found out that this would not be safe, as there might already be a class applied to the body element, which means we need to check for that and append the new class if needed.


document.body.className&nbsp;=&nbsp;(document.body.className&nbsp;+'&nbsp;'||'')&nbsp;+&nbsp;'js';

This made sure all the necessary elements are initially hidden (03_hideandstyle.html). We also pointed out that we use the off-left technique instead of hiding the content with display:none as that makes sure it is still accessible for screen readers.

Event Handling

This is as far as we got with JavaScript and the DOM. Now we needed to find out how we can show the section we want to show when the appropriate link is clicked. We need to use event handling for that and apply event listeners to the different elements. Originally the participants considered adding a click event listener to every event but after going through the idea of event handling using human guinea pigs (video on dailymotion) we realized that there is only need to apply a single event handler to the TOC and use event delegation to do the rest.

We used the W3C approach to event handling, which resulted in the following code (04_events.html):

CSS:


body.js&nbsp;div.show{
position:relative;
left:0;
}

JavaScript:


document.body.className&nbsp;=&nbsp;(document.body.className&nbsp;+'&nbsp;'||'')&nbsp;+&nbsp;'js';
var&nbsp;toc&nbsp;=&nbsp;document.getElementById('toc');
if(toc){
function&nbsp;toggle(e){
var&nbsp;t&nbsp;=&nbsp;e.target;
if(t.nodeName.toLowerCase()&nbsp;=&nbsp;'a'){
var&nbsp;sectionID&nbsp;=&nbsp;t.getAttribute('href').split('#')[1];
var&nbsp;section&nbsp;=&nbsp;document.getElementById(sectionID);
if(section){
section.parentNode.className&nbsp;=&nbsp;'show';
}
}
};
toc.addEventListener('click',toggle,false);
};

This made sure we can actually show the elements when we click on them but it also had the problem that we didn’t hide the previously shown section. For this, we needed to store the information and reset it when toggle() was called. I mentioned as a tip that it is always a good plan to use an object to store state of an interface as that means you can keep as many properties as you want without needing to introduce more variables. The change was only a few lines storing the section in a current object and removing the class when there is already one set (05_keepingstate.html):


document.body.className&nbsp;=&nbsp;(document.body.className&nbsp;+'&nbsp;'||'')&nbsp;+&nbsp;'js';
var&nbsp;toc&nbsp;=&nbsp;document.getElementById('toc');
if(toc){
var&nbsp;current&nbsp;=&nbsp;{};
function&nbsp;toggle(e){
var&nbsp;t&nbsp;=&nbsp;e.target;
if(t.nodeName.toLowerCase()&nbsp;= &#8216;a&#8217;){
if(current.section){
current.section.className = &#8216;&#8217;;
};
var sectionID = t.getAttribute(&#8216;href&#8217;).split(&#8216;#&#8217;)[1];
var section = document.getElementById(sectionID);
if(section){
section.parentNode.className = &#8216;show&#8217;;
current.section = section.parentNode;
};
};
};
toc.addEventListener(&#8216;click&#8217;,toggle,false);
};

This was it in terms of (very basic) functionality for the course (we only had an hour) and the last few things to remember was to make this code work with other scripts and how to make it easier to maintain.

Namespacing the script.

The easiest trick to make the script work well with others is to use the module pattern and nest it in a variable assigned to an anonymous function (06_namespaced.html):


var&nbsp;sectionCollapse&nbsp;=&nbsp;function(){
document.body.className&nbsp;=&nbsp;(document.body.className&nbsp;+'&nbsp;'||'')&nbsp;+&nbsp;'js';
var&nbsp;toc&nbsp;=&nbsp;document.getElementById('toc');
if(toc){
var&nbsp;current&nbsp;=&nbsp;{};
function&nbsp;toggle(e){
var&nbsp;t&nbsp;=&nbsp;e.target;
if(t.nodeName.toLowerCase()&nbsp;=&nbsp;'a'){
if(current.section){
current.section.className&nbsp;=&nbsp;'';
};
var&nbsp;sectionID&nbsp;=&nbsp;t.getAttribute('href').split('#')[1];
var&nbsp;section&nbsp;=&nbsp;document.getElementById(sectionID);
if(section){
section.parentNode.className&nbsp;=&nbsp;'show';
current.section&nbsp;=&nbsp;section.parentNode;
};
};
};
toc.addEventListener('click',toggle,false);
};
}();

Making maintenance easier

In order to make it easy for the next developer taking over from us, we then agreed to not have any ID or class names in the script itself but move them out to an own config object. The last step we were able to cover was to move the CSS and JavaScript out into own documents and our example was done (07_final.html):


var&nbsp;sectionCollapse&nbsp;=&nbsp;function(){

//&nbsp;start&nbsp;configuration&nbsp;-&nbsp;edit&nbsp;here
var&nbsp;config&nbsp;=&nbsp;{
JSavailableClass:'js',
showClass:'show',
tocID:'toc'
}
//&nbsp;end&nbsp;configuration

document.body.className&nbsp;=&nbsp;(document.body.className&nbsp;+'&nbsp;'||'')&nbsp;+&nbsp;config.JSavailableClass;
var&nbsp;toc&nbsp;=&nbsp;document.getElementById(config.tocID);
if(toc){
var&nbsp;current&nbsp;=&nbsp;{};
function&nbsp;toggle(e){
var&nbsp;t&nbsp;=&nbsp;e.target;
if(t.nodeName.toLowerCase()&nbsp;= &#8216;a&#8217;){
if(current.section){
current.section.className = &#8216;&#8217;;
};
var sectionID = t.getAttribute(&#8216;href&#8217;).split(&#8216;#&#8217;)[1];
var section = document.getElementById(sectionID);
if(section){
section.parentNode.className =  config.showClass;
current.section = section.parentNode;
};
};
};
toc.addEventListener(&#8216;click&#8217;,toggle,false);
};
}();

Conclusion

I hope that the participants got an insight how you can make CSS and JavaScript can work together, and learnt some ways to make their JavaScript easier to maintain. I personally wanted most of all people to start analyzing problems before starting the code :)

[tags]parisweb,parisweb2007,workshop,unobtrusive javascript,unobtrusive,coursematerial[/tags]