Building a Live Tile jQuery Plugin

The idea of a jQuery enabled Tile display unit came to me when I was trying to build a Metro Styled UI for my own website. I wanted a basic functionality where a list of items would scroll horizontally and on hover, it would highlight the title text. It should be similar to the following:

 clip_image002 clip_image004
Regular                              Hover

With this requirement in mind, I found a very nice JavaScript at www.fiveminuteargument.com, a cool site by web designer and developer Bobby Jack. I requested Bobby if I could use his sample in my site and he very kindly agreed.
Next the challenge was I wanted the JavaScript to be applied to multiple tiles because I wanted a Windows Phone style UI with 6-8 tiles. Thus came the inspiration to build a jQuery plugin for it. I have used this plugin on a sample ASP.NET MVC site, however you can do it on an HTML page too.

Theory behind a jQuery plugin

jQuery Plugins are not new. The jQuery framework architecture has a well-documented plugin infrastructure. We will quickly step through the basics of what it takes to build a jQuery plugin.

Step 1: Basic Syntax
jQuery plugins are JavaScript function properties added to the jQuery.fn object from the jQuery library. The syntax for it is as follows:
jQuery.fn.liveTile = function() 

{

    // The plugin related methods here  
};

As we see above, liveTile is the name of our plugin. You can replace it with a name of your choice for the plugin.

Step 2: Using the $ symbol

Now we can wrap the above in an immediately invoked function expression (IIFE) so that it maps to the $ sign and it can’t be overwritten by another library in scope of its execution. So our boilerplate code becomes

(function( $ ) 

{     $.fn.extend({  
        liveTile: function() 
  
    {  
        // The plugin related methods here  
    };   
})(jQuery);
  

Step 3: Maintaining Chainability

We want our plugin to be applicable to multiple <div> elements because we will have a grid of 6-8 divs and we want to be able to pass on the ‘liveTile’ along to the next method in the chain. To do this, our plugin must return the ‘this’ keyword. So our shell code is modified to the following

(function ($) {  
$.fn.extend({  
        liveTile: function () {  
            return this.each(function () {  
            var $this = $(this);
  
        // Rest of The plugin related code here  
         });  
     }  
    });  
})(jQuery);

  Now we can internally use the $this variable to refer to the current tile.

Step 4: Sending in External Data and setting up defaults

For each tile, we want to be able to send external list of items so that each tile shows different items. We extend the code as follows to pass an array of strings

(function ($) {  
    var selectedTile;  
    $.fn.extend({  
        liveTile: function (datasource) {  
            var defaults = ['sample'];  
            var options = $.extend(defaults, datasource);
            return this.each(function () {  
                var tileData = options;  
         });  
      }  
    });  
});  

In the above snippet, datasource is an array of strings. If datasource is null i.e. user didn’t pass anything, then the defaults variable is used with the single element array with the value ‘sample’.

Step 5: Scrolling the Items

By default, our ‘tiles’ will show the list of items. On first mouse hover, it will start scrolling. We are saving the timerId so that it is not re-invoked once the scrolling starts. The smoothAdd function written by Bobby adds a <li> item for the string that is passed to it and removes the last <li> item in the list.
The prepend function adds the new item to the top of the list. Hence the scrolling is top to bottom.

(function ($) {  
    var selectedTile;  
    $.fn.extend({  
    liveTile: function (datasource) {
        var defaults = ['sample'];   
        var options = $.extend(defaults, datasource);  
        return this.each(function () {   
            var tileData = options;   
            var $this = $(this);   
            var currentThis = this;   
            var newTilePosition = 0;   
            var timerId = -1;   
            $this.mouseenter(function () {   
            $this.css('cursor', 'pointer')';   
                if (timerId == -1) {   
                    timerId = setInterval(function () {   
                        var itemPos =   
                            tileData.length - newTilePosition - 1;   
                            if (itemPos < 0) {   
                                newTilePosition = 0;   
                          itemPos = tileData.length - newTilePosition - 1;   
                            }


                            var tilecontentid =   
                                $this.find('.tilecontenttext').attr('id');   
                            smoothAdd(tilecontentid, tileData[itemPos]);   
                            newTilePosition++;  
                        }   
                    , 2000);   
                }   
            }); 
  
            
            /*   
            This function has been taken   
            from hxxp://www.fiveminuteargument.com/scrolling-list-demo.html   
            with permission of author.  
            */   
            function smoothAdd(id, text) {   
                var el = $('#' + id);   
                var h = el.height();

                el.css({   
                    height: h,   
                    overflow: 'hidden'   
                });


                var ulPaddingTop = parseInt(el.css('padding-top'));   
                var ulPaddingBottom = parseInt(el.css('padding-bottom'));

                el.prepend('<li>' + text + '</li>');

                var first = $('li:first', el);  
                var last = $('li:last', el);   
                var foh = first.outerHeight();   
                var heightDiff = foh - last.outerHeight();   
                var oldMarginTop = first.css('margin-top');

                first.css({   
                    marginTop: 0 - foh,   
                    position: 'relative',   
                    top: 0 - ulPaddingTop   
                });

                last.css('position', 'relative');

                el.animate({ height: h + heightDiff }, 1500)

                first.animate({ top: 0 }, 250, function () {  
            first.animate({ marginTop: oldMarginTop }, 1000, function () {   
                 last.animate({ top: ulPaddingBottom }, 250, function () {   
                            last.remove();

                            el.css({   
                                height: 'auto',   
                                overflow: 'visible'   
                            });   
                        });   
                    });   
                });   
            }   
});   

Step 6: Highlighting the ‘tile’ and setting up hover effects

The tile applies a css style to highlight the title and provides a translucent layer on the tile that is also applied through CSS.

In our plugin, the div element that contains the tile is referenced through the $this variable.
So we trap the mouseenter, mouseout and mouseclick events in JavaScript and use CSS selector methods to swap out CSS classes as required. The plugin code now gets expanded to the following

The mouseenter handler

We were earlier handling the mouseenter to trigger the scrolling timer. We expand it below for the hover effect.

$this.mouseenter(function ()  
{  
    $this.find('.tilecontent')  
        .removeClass('tilecontenthover')  
        .addClass('tilecontenthover');  
    $this.find('.tilelabel')  
        .removeClass('tilelabelhover')  
        .addClass('tilelabelhover');
  
    $this.css('cursor', 'pointer');  
    if (timerId == -1) {  
    timerId = setInterval(function ()   
    {  
        var itemPos = tileData.length - newTilePosition - 1;  
        if (itemPos < 0)   
        {  
            newTilePosition = 0;  
            itemPos = tileData.length - newTilePosition - 1;  
        }
  
        var tilecontentid = $this.find('.tilecontenttext').attr('id');  
        smoothAdd(tilecontentid, tileData[itemPos]);  
        newTilePosition++;  
    }, 2000);  
    } 
})

  
Markup for a typical ‘tile’ is as follows

<div id="newTile" class="tile tile-singlewidth">  
    <div class="tilecontent tile-singlewidth">  
        <ul id="newTileContent" class="tilecontenttext">  
            <li />  
            <li />  
            <li />  
            <li />  
            <li />  
            <li /> 
        </ul>  
    </div>  
    <div class="tilelabel">  
        What's New  
    </div>  
</div>  

In this, the outer most div is the tile. The div with the class titlecontent contains the text that scrolls. The div with the class titlelable is the item header.
As we can see from the mouseenter event handler, we are swapping out one css for another for the tilecontent div and the tilelabel div. It also sets the mouse cursor to ‘pointer’ which looks like a hand by default.

The click handler

If we click the tile, we want to change the tile’s text to indicate it is selected. The following snippet shows the code for handling the click

.click(function ()   
{  
    tileClickHandler($this);  
});

tileClickHandler = function (parent)   
{  
    selectTile(parent);  
}


selectTile = function (parent)   
{  
    deselectTile(selectedTile);  
       selectedTile = parent;  
       $(parent).find('.tilelabel').addClass('tilelabelhover');  
}


deselectTile = function (parent)   
{  
    if (parent != null)   
    {  
        $(parent).find('.tilelabel').removeClass('tilelabelhover');  
    }  
}

As we see, the click handler calls the tileClickHandler method, which checks if the tile that was clicked was already selected. If yes, it tries to deselect it by removing the required CSS classes in the deselectTile function. After deselecting it sets the current tile to a (plugin level) global variable ‘selectedTile’.

The mouseleave handler

When the mouse hovers out of the tile, we want to remove the hover effect. The following code snippet does this

.mouseleave(function ()  
{ 
    $this.find('.tilecontent').removeClass('tilecontenthover');  
    if ($this != selectedTile)   
    {  
        $this.find('.tilelabel').removeClass('tilelabelhover');  
    }
  
    $this.css('cursor', 'auto');  
})

It removes the hover effect directly. Then it checks if the current tile is the selected tile. If it is, it leaves the text highlighting in place. If not, it removes the text highlighting. It also resets the mouse cursor.

Putting it all together

In the attached code sample, I have created an empty MVC Web Site. Added a default ‘HomeController’. Created a Home folder and added an Index.cshtml file. In the _layouts.cshtml I have provided the header text only. We reference our JavaScript plugin by including the script reference to liveTile-0.0.1.js
Hooking up the Plugin to ‘Tiles’
In the Index.cshtml, we have the grid of five tiles and we hook up our plugin each using the following script block. As we can see, we are referring to each <div> container using it’s id (eg newTile, projectsTile etc.) and adding the list of items we want to see in the tile as an array of strings.
<script type="text/javascript">
  
    $(document).ready(function () {

  
 $('#newTile').liveTile(['blogs', 'projects', 'pictures', 'social media']);

  
        $('#projectsTile').liveTile(['SignalR', 'RavenDB', 'ASP.NET MVC', 

  
                                    'jQuery', 'jQuery Plugins', 'Hadoop']);

  
        $('#blogTile').liveTile([]);

  
        $('#galleryTile').liveTile(['Picasa']);

  
        $('#aboutTile').liveTile(['@@sumitkm']);

  
    });    
</script>

CSS for the liveTile

Following CSS styling is added to our Site.css
#scroller  
{  
    list-style: none;  
    padding: 0em;  
    border: 1px solid #9DB029;  
    margin: 0em 0;  
    text-align: right;  
}

#scroller li  
{  
    border: 1px solid #ddd;  
    width: 173px;  
    margin: 0.0em;  
    padding: 0.0em;  
    background-color: #eee;  
    text-align: right;  
}


.tilecontentheading 
{  
    font-size: 1.6em;  
}


.tilecontenttext  
{  
    /*font-size: 1.2em; */  
    height: 110px;  
    list-style-type: none;  
    overflow: hidden;  
}


.tile  
{  
    float: left;  
    margin-right: 10px;  
    margin-bottom: 10px;  
    height: 175px;  
    background-color: #CFEAFE;  
    font-family: "Segoe UI" ,Helvetica,Garuda,Arial,sans-serif;  
}


.tile-singlewidth  
{  
    width: 175px;  
}


.tile-doublewidth 
{  
    width: 362px;  
}


.tilecontent  
{  
    height: 130px;  
    text-align: right;  
}



.tilecontenthover  
{  
    height: 130px;  
    opacity: .4; 
}


.tilelabel  
{  
    height: 30px;  
    color: #666666;  
    padding-right: 1em;  
    text-transform: lowercase;  
    vertical-align: bottom;  
    text-align: left;  
    font-size: 1.4em;  
}


.tilelabelhover  
{  
    background-color: #666666;  
    color: #ffffff;  
}

The Final View and a Few Gotchas
Finally our plugin renders the tiles as shown below. However, the keen eyed JavaScript ninjas amongst you would have spotted a few gotchas

1. We provided empty <li /> in each tile. The animation code depends on the height of each <li /> so if there are none, it is unable to animate the list items that are added.
2. The scrolling list isn’t exactly a circular queue. As a result, the same item may come in more than once if vertical space permits.
We leave these items as an exercise. The code is available on github and you could fork it and update the repository with your latest changes too.
jquery-livetile-final

Conclusion

We saw how we could build a jQuery plugin that can be applied to multiple DOM elements. We also saw how to create a nice animation effect using CSS and JavaScript.

The entire source code can be downloaded from here.

See a Live Demo




1 comment:

Richard Pierce said...

I am very interested in your post. The information in your post is very benefitable for me. Thanks for share this post.