Random notes by Chris Heilmann

Archive for the ‘api’ Category

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.

Technorati Tags: , , , , , , , ,

Making twitter multilingual with a hack of the Google Translation API

Monday, March 31st, 2008

After helping to fix the Yahoo search result pages with the correct language attributes to make them accessible for screen reader users I was wondering how this could be done with user generated content. The easiest option of course would be to ask the user to provide the right language in the profile, but if you are bilingual like me you actually write in different languages. The other option would be to offer me as the user to pick the language when I type it, which is annoying.

I then stumbled across Google’s Ajax Translation API and thought it should be very easy to marry it with for example the JSON output of the twitter API to add the correct lang attributes on the fly.

Alas, this was not as easy as I thought. On the surface it is very easy to use Google’s API to tell me what language a certain text is likely to be:


var text = "&#191;D&#243;nde est&#225; el ba&#241;o?";
google.language.detect(text, function(result) {
  if (!result.error) {
    var language = 'unknown';
    for (l in google.language.Languages) {
      if (google.language.Languages[l] == result.language) {
        language = l;
        break;
      }
    }
    var container = document.getElementById("detection");
    container.innerHTML = text + " is: " + language + "";
  }
});

However, if you want to use this in a loop you are out of luck. The google.language.detect method fires off an internal XHR call and the result set only gives you an error code, the confidence level, a isReliable boolean and the language code. This is a lot but there is no way to tell the function that gets the results which text was analyzed. It would be great if the API repeated the text or at least allowed you to set a unique ID for the current XHR request.

As Ajax requests return in random order, there is no way of telling which result works for which text, so I was stuck.

Enter Firebug. Analyzing the requests going through I realized there is a REST URL being called by the internal methods of google.language. In the case of translation this is:


http://www.google.com/uds/GlangDetect?callback={CALLBACK_METHOD}&context={NUMBER}&q={URL_ENCODED_TEXT}&key=notsupplied&v=1.0

You can use the number and an own callback method to create SCRIPT nodes in the document getting these results back. The return call is:


CALLBACK_METHOD('NUMBER',{"language" : "es","isReliable" : true,"confidence" : 0.24716422},200,null,200)

However, as I am already using PHP to pull information from another service, I ended up using curl for the whole proof of concept to make twitter speak in natural language:


    <ul>
    <?php
      // curl the twitter feed
      $url = 'http://twitter.com/statuses/public_timeline.rss';
      $ch = curl_init(); 
      curl_setopt($ch, CURLOPT_URL, $url); 
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
      $twitterdata = curl_exec($ch); 
      curl_close($ch); 
      // get all the descriptions
      preg_match_all("/<description>([^<]+)</description>/msi",    $twitterdata,$descs);
      // skip the main feed description
      foreach($descs[1] as $key=>$d){
        if($key===0){
          continue;
        }
        // assemble REST call and curl the result
        $url = 'http://www.google.com/uds/GlangDetect?callback=' .  
               'feedresult&context=' . $key . '&q=' . urlencode($d) .
               '&key=notsupplied&v=1.0';
        $ch = curl_init(); 
        curl_setopt($ch, CURLOPT_URL, $url); 
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
        $langcode = curl_exec($ch); 
        curl_close($ch);
        // get the language
        preg_match("/"language":"([^"]+)"/",$langcode,$res);
        // write out the list item
        echo '<li lang="'.$res[1].'">'.$d.'</li>';
      }
    ?>
    </ul>

Check out the result: Public twitter feed with natural language support

I will do some pure JavaScript solutions soon, too. This could be a great chance to make UGC a lot more accessible.

Thanks to Mark Thomas and Tim Huegdon for bouncing off ideas about how to work around the XHR issue.

Generating charts from accessible data tables and vice versa using the Google Charts API

Tuesday, January 8th, 2008

Google have lately been praised for their chart API and my esteemed colleague Ed Eliot has a workaround for its restrictions in terms of caching and server hits.

I played around a bit with it and thought it very cool but it felt a bit clunky to add all these values to a URL when they could be in the document for those who cannot see pie charts. This is why I wrote a small script that converts data tables to charts using the API and a wee bit of JavaScript.

Using this script you can take a simple, valid and accessible data table like the following and it gets automatically converted to a pie chart.


<table class="tochart size300x100 color990000" summary="Browsers for this site, March 2007">
  <caption>Browsers</caption>
  <thead>
    <tr><th scope="col">Browser</th><th scope="col">Percent</th></tr>
  </thead>
  <tbody>
    <tr><td>Firefox</td><td>60</td></tr>
    <tr><td>MSIE</td><td>25</td></tr>
    <tr><td>Opera</td><td>10</td></tr>
    <tr><td>Safari</td><td>5</td></tr>
  </tbody>
</table>

Simply add the script to the end of the body and it’ll convert all tables with a class called “tochart”. You can define the size (widthxheight) and the colour as a hexadecimal triplet as shown in this example. If you leave size and colour out, the script will use presets you can alter as variables in the script itself.

What about data tables from charts?

As Victor of the Yahoo! Accessibility group asked for the other way around, this is now also possible. When you use the verbose data mode for the charts and add the class “totable” to the image the script will generate a data table preceeding the image and null out the alternative text. For example:


<img src="http://chart.apis.google.com/chart?cht=p3&chco=339933&chs=450x150&chd=t:10,58,95,30,63&chl=Apples|Oranges|Pineapples|Bananas|Pears" alt="Fruit Consumption of under 15 year olds, March 2007" class="totable"/>

The tables have a class called “generatedfromchart” which you can use to move them off-left if needed.

Check out the demo page and download the script with the demo page and CSS to have a go with it yourself. Of course, all is licensed creative commons, so go nuts.

Useful? Please comment if you want something extra or wonder how the script works.

Technorati Tags: , , , , ,

Dear API Developers, this is what I would like to have

Wednesday, November 7th, 2007

Jonathan Boutelle of Slideshare reacted to my slideshare show widget and liked how I hacked around the API by re-using the RSS feed. He now asked in the comments what I’d like to see from an API. Well, here goes:

  1. Allow for “hackable” URLs, with definition of the output. Flickr and Del.icio.us are good examples, especially the del.icio.us option of defining a callback for the JSON: http://del.icio.us/feeds/json/codepo8 gets me a JSON data wrapped in a Delicious object, http://del.icio.us/feeds/json/codepo8?raw gets me the raw JSON data and http://del.icio.us/feeds/json/codepo8?raw&callback=foo wraps it in a function call to foo(). This rocks! The same goes for defining the output as the last parameter. Flickr does that well – http://api.flickr.com/...format=json for JSON, http://api.flickr.com/...format=rss for RSS, http://api.flickr.com/...format=lol for LOLCAT
  2. make sure that the JSON output is easy to use and does not have any annoying bits (encoded HTML or namespaced attributes – the description property in the flickr JSON to me is pointless weight for example)
  3. make the URL as logical as possible, I don’t like to have to use the user ID in flickr for example when the readable user name would be easier to do.
  4. it’d be great if you could send a unique ID as a parameter as that would allow you to match returned data to calls (as both dynamically created script nodes and Ajax calls may return in any order)

However, all of this does not replace the real API, which should

  1. allow me to define only the data bits that I need (and cut down to the smallest possible feed – no twitter, 150kb JSON is not good!)
  2. give me extras when I go through a developer ID. How about offering me free stats (even as an own API) when I build a widget that uses my ID - we do this now to throttle usage anyways. In a second phase this could also be used for a revenue sharing program.
  3. offer things like enforced authentication (you know the photos you don’t want to show your mother)
  4. allow for local caching methods (deliver the data gzipped for example)
  5. allow me access to things that the open REST calls don’t (my sets, my favourites, my contacts, my profile settings)
  6. be read and write – I want to build widgets that allow data entry from my blog to your systems, without leaving it.

Anything else?

Technorati Tags: , , , ,

Showing off your presentation slides with slideshare, PHP and a bit of JavaScript

Wednesday, October 31st, 2007

First of all, I am a big fan of slideshare, a web app that allows you to upload presentations in powerpoint, open office or PDF and share it on the web. Slideshare converts the presentation (sadly enough not 100% when it comes to fonts and kerning :-( ) and people can comment on them, there is a text version of all slides and you can embed the slides in your blog or other sites.

When I checked my slides I had a look at the API of slideshare but I am always a bit bored with having to go through a developer ID and then do everything on the server. That’s why I put on my “ethical hacker” hat and took a look at the RSS feed of my slides and found everything I need there! If you look at the source of the feed you’ll see that it contains not only the titles and descriptions but also the media code, in this case the HTML to embed the right flash movies.

Taking this information it is pretty easy to build a viewer that allows people to click through all your presentations without having to leave your site. This can look something like this:

Interface to click through different slide shows

When JavaScript is available this will be the look and feel and functionality. When JS is turned off all you’ll get is an unstyled list of links pointing to the presentations on slideshare.net.

You can check the slideshare show in action and get a zip to download and use on your site if you don’t want to know how it is done. If you do, read on…

The code necessary is really easy and done in about 70 lines. Let’s go through it bit by bit. I am using PHP4 together with cURL, DOMXML and some JavaScript using the YUI.


<?php
$url = 'http://www.slideshare.net/rss/user/cheilmann';

	

$ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $slides = curl_exec($ch); curl_close($ch);

It starts with the URL we want to load and a CURL call to pull this file and store it in the variable $slides.


$slides = str_replace('slideshare:embed','slideshareembed',$slides);
$slides = str_replace('media:title','mediatitle',$slides);
$xml = domxml_xmltree($slides);

To make things easier (as DOMXML is a terribly hacky piece of kit – much easier with PHP5 and SimpleXML that one) I rename the namespaced attributes in the feed containing the embed code and the title of the media to simple elements and create an object collection from the XML using domxml_xmltree.


$json = array();
$slidesharelist = '';
$links = $xml->get_elements_by_tagname('link');
$img = $xml->get_elements_by_tagname('url');
$titles = $xml->get_elements_by_tagname('mediatitle');
$embeds = $xml->get_elements_by_tagname('slideshareembed');

Then I need to preset an array to contain the embed code for each slides and a string to contain the list of links pointing to presentations on slideshare. I use the get_elements_by_tagname method of DOMXML to get arrays of the different bits of content that I need from the RSS feed.


foreach ($embeds as $key=>$el) {
    $l = $links[$key+2]->children[0]->content;
    $t = $titles[$key]->children[0]->content;
    $slidesharelist .= '<li><a href="'.$l.'">'.$t.'</a></li>';
    $emb = $el->children[0]->content;
    if(strpos($emb,'<div')===false){$emb = $el->children[1]->content;}
    preg_match_all('/.*(<object.*>.*</object>).*/msi',$emb,$obj);
    $json[]='''.$obj[1][0].''';
}
?>

By looping throught the “embeds” array I assembling a list of links pointing to the different presentations and add the embed code to the JSON array. I need this one later to show the different flash movies when visitors click the presentation links. Notice that I need to skip the first two LINK elements as that is the one pointing to the main URL of the RSS feed. For some reason the order of embeds was different on my localhost and my live server, which is why I added that extra if statement. Annoying, that.

That is all the PHP we need! Now it is time to make it pretty and add the rest of the HTML.


<style type="text/css">
  @import 'slideshareshowstyles.css';
  #slideshareshowslideshow{background:url(<?php echo $img[0]->children[0]->content;?>) no-repeat center center;}
</style>
<div id="slideshareshow">
<ul id="slideshareslides"><?php echo $slidesharelist; ?></ul>
</div>

As it is hacky enough to mix PHP and JavaScript I put all the CSS fun in an own document and only add the logo of the RSS feed as the background of the slideshow container. The markup is a main DIV with an unordered list that gets the HTML assembled earlier in the PHP script. This shows the links but doesn’t do the dynamic showing yet. For that we need JavaScript.


<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript">
YAHOO.example.slideshareshow = function(){

Technically we wouldn’t need the YUI for this, but I got quite used to its perks like namespacing and proper event handling without browsers hacks so I just went for it. I also started a namespaced function which uses the module pattern to contain all the variables inside its scope.


    var container = document.getElementById('slideshareshow');
    YAHOO.util.Dom.addClass(container,'jsenabled');
    var list = document.getElementById('slideshareslides');
    var links = list.getElementsByTagName('a');

I get the container element and add a CSS class called “jsenabled”. This allows me to use it as a hook in the CSS file to style only when JavaScript is available. I take the list and get all the links inside it.


    var displayContainer = document.createElement('div');
    displayContainer.id = 'slideshareshowslideshow';
    container.appendChild(displayContainer);
    var current = null;

I then create a new DIV that will contain the Flash movies of the different presentations when the user clicked a link. I give it an ID to allow for styling and append it to the main container element. I predefine “current” as “null”, this will later on be the currently selected link in the list.


    for(var i=0;links[i];i++){
        YAHOO.util.Event.on(links[i],'click',show,i);
    }

I loop through all the links and add an event listener pointing to the show() method and sending the number of the link as a parameter. This allows me to get the correct Flash embedding code from the JSON array I assembled in PHP.


    function show(e,i){
        YAHOO.util.Dom.removeClass(current,'current');
        current = this;
        displayContainer.innerHTML = slides[i];
        YAHOO.util.Dom.addClass(current,'current');
        YAHOO.util.Event.stopEvent(e);
    }

In the show method I remove the “current” CSS class (another hook for styling) from the last link that was clicked and set the new one as current. I set the innerHTML of the container DIV to the right code from the JSON array, add the “current” class to the link that was clicked and stop the link from being followed by invoking stopEvent.

This is where the YUI Event utility rocks, I have the correct number to get as I sent it as an own object, I know which link was clicked as it is stored in “this” and I have the whole event object in e. What more do you need?


    var slides=[<?php echo implode($json,','); ?>];
}();
</script>

That’s all, except for putting the data from the RSS feed into a “slides” array and closing the module pattern.

Together with the right style sheet this is enough to have a clickable list of your latest presentations on slideshare. Enjoy.

Technorati Tags: , , , , , ,