Software developer blog

JavaScript based slide show

Although I think ActionScript is a well designed language one has to admit it's numerous drawbacks: it's a proprietary format, the interpreter is a poorly written black hole of resources and it is banned from Apple devices from time to time. Few years back one of my colleagues went as far as claiming that he only needs a dual core processor, so that one core is free for working while the other one handles YouTube videos. So here is an advice from me: try to avoid using flash as long as possible. JavaScript has it's own set of drawbacks, but at least it is widely available, and with the appearance of Google's Chrome the race between browsers shifted towards improving their JS platforms significantly in terms of speed, compatibility and reliability.

There are more than a handful of websites that include a slide show of images. However most of them uses a flash based solution. As fate would have it, slide shows are a perfect example of unnecessary usage of flash. In what follows I'd like to show you how easy it is to write one in JavaScript. I will use the Prototype and script.aculo.us frameworks for this example, but it should not take you more than a few minutes to make this work with jQuery. (I may even publish a jQuery version soon.) This is how it will look like:

The images are taken randomly from the website
of the Gallery Diabolus artist community.

The slideshow object

The concept is simple enough: we will retrieve a list of images from the server by an AJAX call, than we will recursively make a delayed call to a function which first preloads the image, than displays it. The image list will be provided as a JSON encoded array. (Click here for an example.) Let's start by writing the constructor:

function slideshow(_container, _url)
{
    this.container = $(_container);
    this.url = _url;
    this.imageObj = new Image();
    this.imageObj.onload = this.nextImage.bind(this);
    this.position = 0;
 
    new Ajax.Request(this.url,
    {
        method: 'get',
        onSuccess: this.processList.bind(this)
    });
}

That was pretty straight forward: first I assigned parameters to local variables for later use, created an image object that will be used for preloading, assigned a callback for the onload event of the image object, initialized position to 0, and made a request for the list of images. There are however two things worth noting here. First the use of $ function, that is the identity on DOM elements, while converts strings to DOM elements with the corresponding id. That let's clients of this class specify DOM elements either by the object itself or by it's id. Second the use of bind to make sure that the callback functions nextImage and processList will correctly refer to the current object by this.

Next we need to define the callback function processList:

slideshow.prototype.processList = function(transport)
{
    this.list = transport.responseJSON;
    this.preload(this.list[this.position].imagepath);
}
 
slideshow.prototype.preload = function(_url)
{
    this.imageObj.src = _url;
}

This just assigns the response to a list, and calls the preload function, which simply assigns the url to this.imageObj.src thereby effectively pre-loading the image. This might require some explanation before it becomes clear how it works, and why it works like that.

Let's start with the easy part: why is this.imageObj.src = _url; declared as a separate function instead of in-lining it? The answer lies in the way we will use it later. We will call this method from another function soon, but there we will delay it. It is possible to use lambda functions as callbacks in JavaScript, still I felt that moving this single statement into a separate function yields a more legible source code.

Since we expect the image list as a JSON encoded array, we can rely the responseJSON property, that will return the decoded array represented by the servers response as long as the server specifies the content type as application/json.

Finally note that since the constructor assigned this.nextImage.bind(this) to the onload event of this.imageObj, once the image is loaded the following function will be called:

slideshow.prototype.nextImage = function()
{
    this.container.innerHTML = 
        "<a href='"+this.list[this.position].pageurl+
        "' target='_top'><img style='border:0px;margin-top:"+
        ((this.container.getHeight()-this.imageObj.height)/2)+
        "px' src='"+this.imageObj.src+"'></a>";
 
    this.position = ++this.position % this.list.length;
    var tmpfnc = this.preload.bind(this);
    tmpfnc.delay(2,this.list[this.position].imagepath);
}

Okay! You've got me... this is not the same as the example above, as it won't use fade-appear transitions. But for simplicity let's just look at this first, and than I'll show you how to add transitions.

The first part looks a bit gibberish, and probably I could do better by using some extra variables, but it's not that hard after all. I change the innerHTML of the container to the image with a link. This is where using a JSON encoded array pays of: we could send link targets along with the image paths. The only part worth explanation is ... style='border:0px;margin-top:"+((this.container.getHeight()-this.imageObj.height)/2)+"px;'.... The reason for this part is, that it's tricky to center DOM objects vertically with CSS, so instead it is handled in JS. If you prefer a pure CSS solution, you can leave this part out, and earn my respect. Note however that it's not trivial to get an objects height while remaining cross-browser compatible, so we leave that trouble to prototype by using it's getHeight method.

So far so good, but we still have one remaining job to deal with: loading the next image. First we would like to increment this.position and the ++ operator would get the job done, were it not for the finite length of the image list. Next I'd write something like this.preload.bind(this).delay(2,this.list[this.position].imagepath); to call the pre-load function for the next image after 2 seconds, but this won't work. Instead I used a temporary variable.

Using the slideshow object

Well... it's not that difficult, so I'll just post the HTML file, and let you figure it out yourself:

        <script src="scriptaculous/lib/prototype.js" type="text/javascript"></script>
        <script src="scriptaculous/src/scriptaculous.js" type="text/javascript"></script>
        <script src="slideshow.js" type="text/javascript"></script>
        <script type="text/javascript"></script>

Adding transitions

Finally the transitions. Well... One could go as far as adding two extra parameters to the constructor, so that effects are customizable, but let's be quick and dirty this time. (Though in a production environment I encourage you to go that far!) All we have to do is add a few extra lines to nextImage, while making sure that image replacement happens after fading out and before appearing, while the pre-loading of the next image starts after the current image appeared. That's how it looks like:

slideshow.prototype.nextImage = function()
{
 
    new Effect.Fade(this.container, 
    {    
        afterFinish:function()
        {    
            this.container.innerHTML = 
                "<a href='"+this.list[this.position].pageurl+
                "' target='_top'><img style='border:0px;margin-top:"+
                ((this.container.getHeight()-this.imageObj.height)/2)+
                "px' src='"+this.imageObj.src+"'></a>";
 
            new Effect.Appear(this.container,
            {    
                afterFinish:function()
                {    
                    this.position = ++this.position % this.list.length;
                    var tmpfnc = this.preload.bind(this);
                    tmpfnc.delay(2,this.list[this.position].imagepath);
                }.bind(this)
            });  
        }.bind(this)
    });  
}

Again: beware of the usage of .bind(this) to avoid problems arising from the usage of the this keyword inside callback functions.

That is pretty much all to it. Only 53 lines of javascript, and you have your completely cross platform sideshow.

@ //