How to Mimic the iGoogle Interface

How to Mimic the iGoogle Interface

Tutorial Details
  • Technology: JavaScript, AJAX
  • Difficulty: Intermediate-Advanced
  • Completion Time: 2-3 hours

Twice a month, we revisit some of our readers’ favorite posts from throughout the history of Nettuts+.

In this tutorial, I’ll show you how to create a customizable interface with widgets. The finished product will be a sleek and unobtrusively coded iGoogle-like interface, which has plenty of potential applications!

Finished Project

The Plan

First, let’s list exactly what we’ll be creating here and what features it’ll have:

  • This interface will contain several widgets.
  • Each widget can be collapsed, removed and edited.
  • The widgets can be sorted into the three seperate columns by the user (using a drag and drop technique).
  • The user will be able to edit the color and title of each widget.
  • Each widget can contain any amount of regular HTML content, text, images, flash etc.

Please note that we will only be covering the front-end aspect of the project in this tutorial. Obviously you could integrate this UI with a solid server-side system which could take care of saving preferences and customised widgets.

Since it’s all about the user and because the idea was influenced by iGoogle we’re going to be calling this project ‘iNettuts’.

The layout of iNettuts

The layout is a simple three column one; each column contains widgets:

layout

Each widget has a "handle" which the user can use to move the widget around.

jQuery UI

As well as the jQuery core library we’re also going to make use of the jQuery’s UI library and specifically the "sortable" and "draggable" modules. This will make it quite simple to add the drag-and-drop functionality that we want. You should get a personalized download of the UI library which has what we need in it. (Tick the ‘sortable’ box)


Step 1: XHTML markup

Each column will be an unordered list (UL) and each widget within the columns will be a list item (LI):

First column:

<ul id="column1" class="column">

    <li class="widget red">  
        <div class="widget-head">
            <h3>Widget title</h3>
        </div>
        <div class="widget-content">

            <p>The content...</p>
        </div>
    </li>
    <li class="widget blue">  
        <div class="widget-head">

            <h3>Widget title</h3>
        </div>
        <div class="widget-content">
            <p>The content...</p>

        </div>
    </li>
</ul>

The above code represents the first column, on the left and two widgets each within a list item. As shown in the plan, there will be three columns – three unordered lists.


Step 2: CSS

We’ll be using two CSS StyleSheets, one of them will contain all the main styles and the second StyleSheet will only contain styles required by the JavaScript enhancements. The reason we seperate them like this is so that people without JavaScript enabled do not waste their bandwidth downloading styles which they’re not going to use.

Here is inettuts.css:

/* Reset */
body,img,p,h1,h2,h3,h4,h5,h6,ul,ol {margin:0; padding:0; list-style:none; border:none;}
/* End Reset */
	
body {font-size:0.8em; font-family:Arial,Verdana,Sans-Serif; background: #000;}
a {color:white;}
	
/* Colours */
.color-yellow {background:#f2bc00;}
.color-red    {background:#dd0000;}
.color-blue   {background:#148ea4;}
.color-white  {background:#dfdfdf;}
.color-orange {background:#f66e00;}
.color-green  {background:#8dc100;}
.color-yellow h3,.color-white h3,.color-green h3
	{color:#000;}
.color-red h3,.color-blue h3,.color-orange h3
	{color:#FFF;}
/* End Colours */
	
/* Head section */
#head {
    background: #000 url(img/head-bg.png) repeat-x;
    height: 100px;
}
#head h1 {
    line-height: 100px;
    color: #FFF;
    text-align: center;
    background: url(img/inettuts.png) no-repeat center;
    text-indent: -9999em
}
/* End Head Section */
	
/* Columns section */
#columns .column {
    float: left;
    width: 33.3%;
		/* Min-height: */
		min-height: 400px;
		height: auto !important; 
		height: 400px;
}
	
/* Column dividers (background-images) : */
	#columns #column1 { background: url(img/column-bg-left.png) no-repeat right top; }
	#columns #column3 { background: url(img/column-bg-right.png) no-repeat left top; }
		
#columns #column1 .widget { margin: 30px 35px 30px 25px; }
#columns #column3 .widget { margin: 30px 25px 30px 35px; }
#columns .widget {
    margin: 30px 20px 0 20px;
    padding: 2px;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
}
#columns .widget .widget-head {
    color: #000;
    overflow: hidden;
    width: 100%;
    height: 30px;
    line-height: 30px;
}
#columns .widget .widget-head h3 {
    padding: 0 5px;
    float: left;
}
#columns .widget .widget-content {
    background: #333 url(img/widget-content-bg.png) repeat-x;
    padding: 5px;
    color: #DDD;
    -moz-border-radius-bottomleft: 2px;
    -moz-border-radius-bottomright: 2px;
    -webkit-border-bottom-left-radius: 2px;
    -webkit-border-bottom-right-radius: 2px;
    line-height: 1.2em;
    overflow: hidden;
}
/* End Columns section */

There’s nothing too complicated in the above StyleSheet. Normally it would be better to use images instead of the CSS3 border-radius property to create rounded corners (for cross-browser benefits) but they’re not really an integral part of the layout – adding a border-radius is quick and painless.

Just a note about the colour classes: Ideally, elements should be named according to their semantic meaning or content, not their appearance. The problem is that the widgets can mean/contain many different things so having classes like this really is the best alternative, unless you’re willing to add the colour styles inline. Each colour class is prefixed with ‘color-’; it’ll become clear why I’ve done this later in the tutorial.

In the above CSS we’re also using a min-height hack for each column so that the background images (the dividers) remain intact and so that an empty column can still have widgets dragged back into it:

#columns .column {
    float: left;
    width: 33.3%;
	
	/* Min-height: */
		min-height: 400px;
		height: auto !important; 
		height: 400px;
}

We’ll focus on the second stylesheet later when we’ve added the JavaScript.

Here’s a preview of what we’ve got so far, just CSS/HTML (and some images):

layout

Step 3: JavaScript

Introduction

As I’ve said, we’ll be using jQuery. It’s the library of choice not only because of the UI modules it offers but also because it will help in speeding up the development process while keeping everything cross-browser operable.

The final product will have endless possibilities, some of which have already been explored by the likes of NetVibes and iGoogle. So, we want to make sure our code is easily maintainable, allows for expandability and is reusable; we want it to be future-proof!

We’ll begin with a global object called "iNettuts" – this will act as the sole occupied namespace of the project (plus dependencies such as jQuery). Under it we will code up the main functionality of the site which utilises jQuery and its UI library.

inettuts.js:

var iNettuts = {
	settings : {
	   // Some simple settings will go here.
	},
	init : function(){
		// The method which starts it all...
	}
};

The init method will be called when the document is ready for manipulation (i.e. when the DOM is loaded and ready). While there are various methods available it has been proven that the quickest way to initialise your code upon this event is to call it from the bottom of your document. It also makes sense to link to all the scripts at the bottom so as not to slow down the loading of the rest of the page:

<body>
    
    <!-- page content -->

    
	
    <!-- Bottom of document -->
    <script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.2.6.min.js"></script>
    <script type="text/javascript" src="inettuts.js"></script>

    <script type="text/javascript" src="jquery-ui-personalized-1.6rc2.min.js"></script>
    
</body>

Settings

As I’ve said, there will be a settings object which will contain all of the global settings required to make this functional. We’ll also have individual widget settings objects, this means it will be possible to create per-widget settings.

settings object (under iNettuts):

settings : {
	/* Specify selectors */
	columns : '.column',
	widgetSelector: '.widget',
	handleSelector: '.widget-head',
	contentSelector: '.widget-content',
	/* Settings for the Widgets: */
	widgetDefault : {
		movable: true,
		removable: true,
		collapsible: true,
		editable: true,
		colorClasses : ['yellow','red','blue','white','orange','green']
	},
	/* Individual Widget settings: */
	widgetIndividual : {
		intro : {
			movable: false,
			removable: false,
			collapsible: false
		},
		gallery : {
			colorClasses : ['yellow','red','white']
		}
	}
}

Yes, there are quite a lot of settings, but if we want maximum code reusability this is a necessity. Most of the above is self explanatory. As you can see we’ve setup a widgetDefault object which contains the default settings for each widget; if you want to override these settings then the script will require you to give the widget an id (in the HTML) and then create a new ruleset. We’ve got two rule-sets (objects) which override their defaults, ‘intro‘ and ‘gallery‘. So, those rules specified in the “gallery” object will only apply to this widget:

<li class="widget blue" id="gallery">  
    <div class="widget-head">
        <h3>Instructions</h3>
    </div>

    <div class="widget-content">
        <ul>
            <li>To move a widget...</li>
        </ul>
    </div>

</li>

Retrieving the settings

getWidgetSettings object (under iNettuts):

getWidgetSettings : function(id) {
	var settings = this.settings;
	return (id&&settings.widgetIndividual[id]) ? 
		$.extend({},settings.widgetDefault,settings.widgetIndividual[id])
		: settings.widgetDefault;
}

This method will return an object with the settings of any particular widget. If a widget has no id (HTML attribute) then it will just return the default settings, otherwise it will look to see if that widget has settings of its own, if it does then it will return the default settings and that widget’s settings merged into a single object (the widget’s individual settings have precedence).

Attaching a CSS file using JavaScript

I mentioned earlier that we have an additional stylesheet which the JavaScript enhancements will require.

Here’s the StyleSheet (inettuts.js.css):

/* JS-Enabled CSS */
	
.widget-head a.remove  {
    float: right;
    display: inline;
    background: url(img/buttons.gif) no-repeat -24px 0;
    width: 14px;
    height: 14px;
    margin: 8px 4px 8px 0;
    text-indent: -9999em;
    outline: none;
}
	
.widget-head a.edit  {
    float: right;
    display: inline;
    background: url(img/buttons.gif) no-repeat;
    width: 24px;
    height: 14px;
    text-indent: -9999em;
    margin: 8px 4px 8px 4px;
    outline: none;
}
	
.widget-head a.collapse  {
    float: left;
    display: inline;
    background: url(img/buttons.gif) no-repeat -52px 0;
    width: 14px;
    height: 14px;
    text-indent: -9999em;
    margin: 8px 0 8px 4px;
    outline: none;
}
	
.widget-placeholder { border: 2px dashed #999;}
#column1 .widget-placeholder { margin: 30px 35px 0 25px; }
#column2 .widget-placeholder { margin: 30px 20px 0 20px; }
#column3 .widget-placeholder { margin: 30px 25px 0 35px; }
	
.edit-box {
    overflow: hidden;
    background: #333 url(img/widget-content-bg.png) repeat-x;
    margin-bottom: 2px;
    padding: 10px 0;
}
	
.edit-box li.item {
    padding: 10px 0;
    overflow: hidden;
    float: left;
    width: 100%;
    clear: both;
}
	
.edit-box label {
    float: left;
    width: 30%;
    color: #FFF;
    padding: 0 0 0 10px;
}
	
.edit-box ul.colors li {
    width: 20px;
    height: 20px;
    border: 1px solid #EEE;
    float: left;
    display: inline;
    margin: 0 5px 0 0;
    cursor: pointer;
}

The elements targetted in the above StyleSheet have yet to be coded, but eventually we will write the JavaScript which dynamically adds these elements to the page, thus making use of the StyleSheet.

The method which attaches this StyleSheet is called ‘attachStylesheet’:

attachStylesheet : function (href) {
    return $('<link href="' + href + '" rel="stylesheet" type="text/css" />').appendTo('head');
}

The above method appends a link to the head of the document. When a new link element is added to the document through the DOM the browser will load it and apply its CSS rules as it would for any regular hard-coded linked StyleSheet. When doing this, remember that the rules of CSS inheritance and specificity still apply.

Making the widgets work

Widgets

The next part of the tutorial is probably the hardest, so take it slowly.

We want to add another method to our global iNettuts object, we’ll call it makeSortable:

makeSortable : function () {
    // This function will make the widgets 'sortable'!
}

By the way, ‘method’ is just a fancy name given to ‘functions’ which have been assigned to object properties. In this case our object is called 'iNettuts' so 'makeSortable' is a method of 'iNettuts'

This new method will take the settings we specified in the 'settings' object and make the required element sortable.

First, we want to make sure everything we need is easily accessible within this new method:

makeSortable : function () {
    var iNettuts = this, // *1
        $ = this.jQuery, // *2
        settings = this.settings; // *3
}

*1: There will only be one instance of our global object, but just encase multiple instances need to be made or if we want to rename the global object it’s a good idea to set a new variable (in this case ‘iNettuts’) to the ‘this’ keyword which references the object that this method is within. Be careful, the ‘this’ keyword is a bit of a beast and doesn’t always reference what you think it does!

*2: At the very top of the iNettuts object we’ve placed a new property: ‘jQuery : $‘. In the pursuit of maximum code reusability we don’t want our script to conflict with any other libraries that also make use of the dollar-sign symbol (e.g. The Prototype library). So, for example, if you renamed jQuery to JQLIB then you could change the ‘jQuery’ property to JQLIB and the script would continue to function properly. The 2nd line in the above code isn’t necessary at all, – if we didn’t want it we could just use this.jQuery().ajQueryFunction() instead of $() within this method.

*3: Again, this isn’t really necessary, we’re just creating a bit of a shortcut, so instead of having to type out ‘this.settings‘ within this method we only need to type out ‘settings‘.

The next step is to define a set of sortable items (i.e. the widgets that will be movable). Remember, back in the settings we made it possible to set a property called 'movable' to true or false. If ‘movable’ is set to false, either by default or on individual widgets we have to cater for that:

/*
 * (using the dollar prefix on $sortableItems is a convention when a variable references a jQuery object)
 */
  
$sortableItems = (function () {
    
    // Define an empty string which can add to within the loop:
    var notSortable = '';
    
    // Loop through each widget within the columns:
    $(settings.widgetSelector,$(settings.columns)).each(function (i) {
        
        // If the 'movable' property is set to false:
        if (!iNettuts.getWidgetSettings(this.id).movable) {
            
            // If this widget has NO ID: 
            if(!this.id) {
                
                // Give it an automatically generated ID:
                this.id = 'widget-no-id-' + i;
                
            }
        
            // Add the ID to the 'notSortable' string:
            notSortable += '#' + this.id + ',';
        }
        
    });
    
    /*
    * This function will return a jQuery object containing
    * those widgets which are movable.
    */
    return $('> li:not(' + notSortable + ')', settings.columns);
})();

Now we’ve got a set of DOM elements referenced in the jQuery object which is returned from the above functions. We can make immediate use of this:

$sortableItems.find(settings.handleSelector).css({
	cursor: 'move'
}).mousedown(function (e) {
	$(this).parent().css({
		width: $(this).parent().width() + 'px'
	});
}).mouseup(function () {
	if(!$(this).parent().hasClass('dragging')) {
		$(this).parent().css({width:''});
	}
});

So, we’re looking for what has been defined as the ‘handle’ within the movable widgets (within sortableItems) and then we’re applying a new CSS cursor property of ‘move’ to each one; this is to make it obvious that each widget is movable.

The mousedown and mouseup functions are needed to work around some issues with dragging and dropping… Since we want this page and all the elements inside it to expand when the browser is resized we haven’t set any explicit widths on the widgets (list items). When one of these list items is being sorted it becomes absolutely positioned (while being dragged) which means that it will stretch to its content’s composite width. Here’s an example:

When dragged the widget stretches to length of page

This is what should be happening:

When dragged the widget has the correct width!

To make this happen we have explicitly set the widget’s width to what it was before dragging had begun. The UI 'sortable' module does have a property in which you can put a function that will run when a widget starts being sorted (i.e. when it starts being dragged), unfortunately this is not good enough for us because it runs too late; we need to set the width before the ‘sortable’ module takes hold – the best way to do this is by running a function on mousedown of the handle (the ‘handle’, in this case, is the bar at the top of each widget).

// mousedown function:
// Traverse to parent (the widget):
$(this).parent().css({
    // Explicitely set width as computed width:
    width: $(this).parent().width() + 'px'
});

If we leave it like this then when you drop the widget in a certain place and res

the browser the widget will not change in size. In order to prevent this we need to write a function to be tied to the mouseup event of the handle:

// mouseup function:
// Check if widget is currently in the process of dragging:
if(!$(this).parent().hasClass('dragging')) {
    // If it's not then reset width to '':
    $(this).parent().css({width:''});
} else {
    // If it IS currently being dragged then we want to 
    // temporarily disable dragging, while widget is
    // reverting to original position.
    $(settings.columns).sortable('disable');
}

The ‘dragging’ class is added on that ‘start’ property of the sortable module which we talked about earlier. (we’ll write that code later)

This is what our makeSortable method looks like so far:

makeSortable : function () {
    var iNettuts = this,
        $ = this.jQuery,
        settings = this.settings,
        
        $sortableItems = (function () {
            var notSortable = '';
            $(settings.widgetSelector,$(settings.columns)).each(function (i) {
                if (!iNettuts.getWidgetSettings(this.id).movable) {
                    if(!this.id) {
                        this.id = 'widget-no-id-' + i;
                    }
                    notSortable += '#' + this.id + ',';
                }
            });
            return $('> li:not(' + notSortable + ')', settings.columns);
        })();
    
    $sortableItems.find(settings.handleSelector).css({
        cursor: 'move'
    }).mousedown(function (e) {
        $sortableItems.css({width:''});
        $(this).parent().css({
            width: $(this).parent().width() + 'px'
        });
    }).mouseup(function () {
        if(!$(this).parent().hasClass('dragging')) {
            $(this).parent().css({width:''});
        } else {
            $(settings.columns).sortable('disable');
        }
    });
}

Next, still within 'makeSortable' we need to initialise the 'sortable' module:

makeSortable : function () {
    // ...........................
    // BEGINNING OF METHOD (above)
    // ...........................
    
    // Select the columns and initiate 'sortable':
    $(settings.columns).sortable({
    
        // Specify those items which will be sortable:
        items: $sortableItems,
        
        // Connect each column with every other column:
        connectWith: $(settings.columns),
        
        // Set the handle to the top bar:
        handle: settings.handleSelector,
        
        // Define class of placeholder (styled in inettuts.js.css)
        placeholder: 'widget-placeholder',
        
        // Make sure placeholder size is retained:
        forcePlaceholderSize: true,
        
        // Animated revent lasts how long?
        revert: 300,
        
        // Delay before action:
        delay: 100,
        
        // Opacity of 'helper' (the thing that's dragged):
        opacity: 0.8,
        
        // Set constraint of dragging to the document's edge:
        containment: 'document',
        
        // Function to be called when dragging starts:
        start: function (e,ui) {
            $(ui.helper).addClass('dragging');
        },
        
        // Function to be called when dragging stops:
        stop: function (e,ui) {
        
            // Reset width of units and remove dragging class:
            $(ui.item).css({width:''}).removeClass('dragging');
            
            // Re-enable sorting (we disabled it on mouseup of the handle):
            $(settings.columns).sortable('enable');
            
        }
        
    });
    
}

The above options setup the behaviour we want for our sortable widgets. There are plenty more available options for this module but those above will be sufficient for now.

Editing, removing and collapsing widgets

The next step is to make it possible for the user to collapse widgets, close (remove) widgets and edit certain elements within each widget.

We’re going to put this all within one method, we’ll call it 'addWidgetControls':


addWidgetControls : function () {
    // This function will add controls to each widget!
}

As with 'makeSortable' we want to set the following variables at the start:

addWidgetControls : function () {
    var iNettuts = this,
        $ = this.jQuery,
        settings = this.settings;
}

We need to loop through every widget on the page and add functionality dependent on the default settings or the settings made for any particular widget.

// Loop through each widget:
$(settings.widgetSelector, $(settings.columns)).each(function () {

	/* Merge individual settings with default widget settings */
	var thisWidgetSettings = iNettuts.getWidgetSettings(this.id);
	
	// (if "removable" option is TRUE):
	if (thisWidgetSettings.removable) {
	
		// Add CLOSE (REMOVE) button & functionality
		
	}
	
	// (if "removable" option is TRUE):
	if (thisWidgetSettings.editable) {
	
		// Add EDIT button and functionality
		
	}
	
	// (if "removable" option is TRUE):
	if (thisWidgetSettings.collapsible) {
	
		// Add COLLAPSE button and functionality
		
	}
		
});

As you can see from the above code, we’re checking the settings before adding any one of the three buttons and each button’s corresponding functionality.

Before we write out exactly what will happen within each of three conditions let’s list exactly what each of these buttons will do:

  • CLOSE (remove): This button will remove the widget from the DOM. Instead of just removing it immediately we’ll apply an effect which will fade out he widget and then slide up its occupied space.
  • EDIT: This button, when clicked, will bring up an ‘edit box’ section within the widget. Within this ‘edit’ section the user can change the title of the widget and its colour. To close the ‘edit’ section the user must click on the same ‘edit’ button again – so basically this button toggles the ‘edit’ section.
  • COLLAPSE: This button switches between an up-arrow and a down-arrow dependent whether the widget is collapsed or not. Collapsing a widget will simply hide its content, so the only viewable of the widget will be the handle (the bar at the top of each widget).

We know what we want now, so we can start writing it: (The snippets below are riddles with comments so make sure you read through the code!)

CLOSE (remove):

// (if "removable" option is TRUE):
if (thisWidgetSettings.removable) {
    
    // Create new anchor element with class of 'remove':
    $('<a href="#" class="remove">CLOSE</a>').mousedown(function (e) {
    
        // Stop event bubbling:
        e.stopPropagation(); 
           
    }).click(function () {
    
        // Confirm action - make sure that the user is sure:
        if(confirm('This widget will be removed, ok?')) {
        
            // Animate widget to an opacity of 0:
            $(this).parents(settings.widgetSelector).animate({
                opacity: 0    
            },function () {
            
                // When animation (opacity) has finished:
                // Wrap in DIV (explained below) and slide up:
                $(this).wrap('<div/>').parent().slideUp(function () {
                
                    // When sliding up has finished, remove widget from DOM:
                    $(this).remove();
                    
                });
            });
        }
        
        // Return false, prevent default action:
        return false;
        
    })
    
    // Now, append the new button to the widget handle:
    .appendTo($(settings.handleSelector, this));
    
}

EDIT:

/* (if "editable" option is TRUE) */
if (thisWidgetSettings.editable) {
    
    // Create new anchor element with class of 'edit':
    $('<a href="#" class="edit">EDIT</a>').mousedown(function (e) {
        
        // Stop event bubbling
        e.stopPropagation();
        
    }).toggle(function () {
        // Toggle: (1st state):
        
        // Change background image so the button now reads 'close edit':
        $(this).css({backgroundPosition: '-66px 0', width: '55px'})
            
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
                
                // Find the edit-box, show it, then focus <input/>:
                .find('.edit-box').show().find('input').focus();
                
        // Return false, prevent default action:
        return false;
        
    },function () {
        // Toggle: (2nd state):
        
        // Reset background and width (will default to CSS specified in StyleSheet):
        $(this).css({backgroundPosition: '', width: ''})
            
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
                
                // Find the edit-box and hide it:
                .find('.edit-box').hide();
        // Return false, prevent default action:
        return false;

    })
    
    // Append this button to the widget handle:
    .appendTo($(settings.handleSelector,this));
    
    // Add the actual editing section (edit-box):
    $('<div class="edit-box" style="display:none;"/>')
        .append('<ul><li class="item"><label>Change the title?</label><input value="' + $('h3',this).text() + '"/></li>')
        .append((function(){
            
            // Compile list of available colours:
            var colorList = '<li class="item"><label>Available colors:</label><ul class="colors">';
            
            // Loop through available colors - add a list item for each:
            $(thisWidgetSettings.colorClasses).each(function () {
                colorList += '<li class="' + this + '"/>';
            });
            
            // Return (to append function) the entire colour list:
            return colorList + '</ul>';
            
        })())
        
        // Finish off list:
        .append('</ul>')
        
        // Insert the edit-box below the widget handle:
        .insertAfter($(settings.handleSelector,this));
        
}

COLLAPSE:

// (if 'collapsible' option is TRUE) 
if (thisWidgetSettings.collapsible) {
    
    // Create new anchor with a class of 'collapse':
    $('<a href="#" class="collapse">COLLAPSE</a>').mousedown(function (e) {
        
        // Stop event bubbling:
        e.stopPropagation();
        

    }).toggle(function () {
        // Toggle: (1st State):
        
        // Change background (up-arrow to down-arrow):
        $(this).css({backgroundPosition: '-38px 0'})
        
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
                // Find content within widget and HIDE it:
                .find(settings.contentSelector).hide();
                
        // Return false, prevent default action:
        return false;
        
    },function () {
        // Toggle: (2nd State):
        
        // Change background (up-arrow to down-arrow):
        $(this).css({backgroundPosition: ''})
        
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
            
                // Find content within widget and SHOW it:
                .find(settings.contentSelector).show();
                
        // Return false, prevent default action:
        return false;
        
    })
    
    // Prepend that 'collapse' button to the widget's handle:
    .prependTo($(settings.handleSelector,this));
}

What is "Event Bubbling"?

Event bubbling or ‘propagation’ is when, upon clicking on an element, the event will bubble up through the DOM to the highest level element with an event the same as the event you just triggered on the original element. If we didn’t stop propogation in the above snippets (e.stopPropagation();) on the mouseDown event of each added button then the mouseDown event of the handle (parent of the buttons) would also trigger and thus the dragging would would begin just by holding your mouse down over one of the buttons – we don’t want this to happen; we only want dragging to begin when the user puts their mouse directly over the handle and pushes down.

Edit-box events/functionality

We’ve written the code which will inject the edit boxes into the document in the correct places. – We added an input box so users can change the title of a widget and we also added a list of available colours. So, we now need to loop through each new edit-box (hidden from view) and specify how these elements can be interacted with:

// Loop through each edit-box (under each widget that has an edit-box)
$('.edit-box').each(function () {
    
    // Assign a function to the onKeyUp event of the input:
    $('input',this).keyup(function () {
        
        // Traverse UP to widget and find the title, set text to
        // the input element's value - if the value is longer
        // than 20 characters then replace remainder characters
        // with an elipsis (...).
        $(this).parents(settings.widgetSelector).find('h3').text( $(this).val().length>20 ? $(this).val().substr(0,20)+'...' : $(this).val() );
        
    });
    
    // Assing a function to the Click event of each colour list-item:
    $('ul.colors li',this).click(function () {
        
        // Define colorStylePattern to match a class with prefix 'color-':
        var colorStylePattern = /\bcolor-[\w]{1,}\b/,
            
            // Define thisWidgetColorClass as the colour class of the widget:
            thisWidgetColorClass = $(this).parents(settings.widgetSelector).attr('class').match(colorStylePattern)
        // If a class matching the pattern does exist:
        if (thisWidgetColorClass) {
            
            // Traverse to widget:
            $(this).parents(settings.widgetSelector)
            
                // Remove the old colour class:
                .removeClass(thisWidgetColorClass[0])
                
                // Add new colour class (n.b. 'this' refers to clicked list item):
                .addClass($(this).attr('class').match(colorStylePattern)[0]);
                
        }
        
        // Return false, prevent default action:
        return false;
        
    });
});

The edit-boxes are entirely functional now. All the above code resides in the ‘addWidgetControls‘ method.

addWidgetControls : function () {
    var iNettuts = this,
        $ = this.jQuery,
        settings = this.settings;
        
    $(settings.widgetSelector, $(settings.columns)).each(function () {
        var thisWidgetSettings = iNettuts.getWidgetSettings(this.id);
        
        if (thisWidgetSettings.removable) {
            $('<a href="#" class="remove">CLOSE</a>').mousedown(function (e) {
                e.stopPropagation();    
            }).click(function () {
                if(confirm('This widget will be removed, ok?')) {
                    $(this).parents(settings.widgetSelector).animate({
                        opacity: 0    
                    },function () {
                        $(this).wrap('<div/>').parent().slideUp(function () {
                            $(this).remove();
                        });
                    });
                }
                return false;
            }).appendTo($(settings.handleSelector, this));
        }
        
        if (thisWidgetSettings.editable) {
            $('<a href="#" class="edit">EDIT</a>').mousedown(function (e) {
                e.stopPropagation();    
            }).toggle(function () {
                $(this).css({backgroundPosition: '-66px 0', width: '55px'})
                    .parents(settings.widgetSelector)
                        .find('.edit-box').show().find('input').focus();
                return false;
            },function () {
                $(this).css({backgroundPosition: '', width: ''})
                    .parents(settings.widgetSelector)
                        .find('.edit-box').hide();
                return false;
            }).appendTo($(settings.handleSelector,this));
            $('<div class="edit-box" style="display:none;"/>')
                .append('<ul><li class="item"><label>Change the title?</label><input value="' + $('h3',this).text() + '"/></li>')
                .append((function(){
                    var colorList = '<li class="item"><label>Available colors:</label><ul class="colors">';
                    $(thisWidgetSettings.colorClasses).each(function () {
                        colorList += '<li class="' + this + '"/>';
                    });
                    return colorList + '</ul>';
                })())
                .append('</ul>')
                .insertAfter($(settings.handleSelector,this));
        }
        
        if (thisWidgetSettings.collapsible) {
            $('<a href="#" class="collapse">COLLAPSE</a>').mousedown(function (e) {
                e.stopPropagation();    
            }).toggle(function () {
                $(this).css({backgroundPosition: '-38px 0'})
                    .parents(settings.widgetSelector)
                        .find(settings.contentSelector).hide();
                return false;
            },function () {
                $(this).css({backgroundPosition: ''})
                    .parents(settings.widgetSelector)
                        .find(settings.contentSelector).show();
                return false;
            }).prependTo($(settings.handleSelector,this));
        }
    });
    
    $('.edit-box').each(function () {
        $('input',this).keyup(function () {
            $(this).parents(settings.widgetSelector).find('h3').text( $(this).val().length>20 ? $(this).val().substr(0,20)+'...' : $(this).val() );
        });
        $('ul.colors li',this).click(function () {
            
            var colorStylePattern = /\bcolor-[\w]{1,}\b/,
                thisWidgetColorClass = $(this).parents(settings.widgetSelector).attr('class').match(colorStylePattern)
            if (thisWidgetColorClass) {
                $(this).parents(settings.widgetSelector)
                    .removeClass(thisWidgetColorClass[0])
                    .addClass($(this).attr('class').match(colorStylePattern)[0]);
            }
            return false;
            
        });
    });
    
}


Almost finished

Now that we’ve written most of the JavaScript we can write the initiating method and intialise the script!

// Additional method within 'iNettuts' object:
init : function () {
	this.attachStylesheet('inettuts.js.css');
	this.addWidgetControls();
	this.makeSortable();
}

Now, to start it all:

// Right at the very end of inettuts.js
iNettuts.init();

Just so we’re clear, this is the overall structure of our iNettuts object with each of its methods explained:

var iNettuts = {
    
    /* Set's jQuery identifier: */
    jQuery : $,
    
    settings : {
        
		/*    Name : settings
		 *    Type : Object
		 * Purpose : Object to store preferences for widget behaviour
		 */
		 
    },

    init : function () {
        
		/*    Name : init
		 *    Type : Function
		 * Purpose : Initialise methods to be run when page has loaded.
		 */
		 
    },
    
    getWidgetSettings : function (id) {
        
		/*      Name : getWidgetSettings
		 *      Type : Function
		 * Parameter : id of widget
		 *   Purpose : Get default and per-widget settings specified in 
		 *             the settings object and return a new object
		 *             combining the two, giving per-widget settings
		 *             precedence obviously.
		 */
		 
    },
    
    addWidgetControls : function () {
        
		/*    Name : settings
		 *    Type : Function
		 * Purpose : Adds controls (e.g. 'X' close button) to each widget.
		 */
		 
    },
    
    attachStylesheet : function (href) {
        
		/*      Name : settings
		 *      Type : Function
		 * Parameter : href location of stylesheet to be added
		 *   Purpose : Creates new link element with specified href and 
		 *             appends to <head>
		 */
		 
    },
    
    makeSortable : function () {
        
		/*    Name : settings
		 *    Type : Function
		 * Purpose : Makes widgets sortable (draggable/droppable) using
		 *           the jQuery UI 'sortable' module.
		 */
		 
    }
  
};

 Finished!

Finished Project

We’re totally finished, the interface should be totally operable now. I’ve tested it on my PC (running Windows XP) in the following browsers: Firefox 2, Firefox 3, Opera 9.5, Safari 3, IE6, IE7 & Chrome.

Note: There are a couple of issues in IE. Specifically, it doesn’t set the placeholder size correctly plus there are some CSS issues in IE6 (to be expected).

At first glance this interface’s potential applications seem limited to those like iGoogle or NetVibes but it can, in fact, be used for many different things.

  • You could, for example use it on your blog by giving the user the option to sort your blog’s widgets in the sidebar – you could then save their preference to a cookie so that the widgets would be in the same order when the user returns.
  • If you add a user authentication system and a database then you’ve got yourself a simple iGoogle.
  • The ‘sortable’ plugin itself can be used for sorting any elements, they don’t have to be widgets.

Regardless of whether you’re going to use this in a project or not I hope you’ve learnt something today!

James Padolsey is JimmyP on Codecanyon
Tags: jQuery
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Pingback: JQuery customizable and dinamic interface – iGoogle style | Design Freebies

  • http://www.fantazo.com Balazs

    Useful tutorial, about these widgets, and interfaces.
    Thank you!

  • Pingback: Our solution to Facebook’s UI problems: custom layouts : delOmni – Home for geeks

  • Cüneyt

    Awesome tutorial, many thanks!

    What about the licensing? May we use this code in a commercial product?

  • Cüneyt

    I have 2 fixes for IE, so that the placeholder is shown with a dashed border and no text gets selected during dragging (tested only with IE7):

    Add a new function to iNettuts object:

    disableTextSelection : function (targets) {
    $(targets).each(function(){
    if (‘undefined’ != typeof this.onselectstart) { // onselectstart is an IE-specific event
    this.onselectstart = function(){ return false };
    }
    });
    }

    and add this to init():
    init : function () {
    // prevent IE to select text during dragging
    this.disableTextSelection(this.settings.columns);
    }

    Also, IE seems not to respect the placeholder class, but you can add the following as workaround:

    start: function (e,ui) {
    // …
    $(ui.placeholder).css({
    height: $(ui.item).height()+’px’,
    width: $(ui.item).width()+’px’,
    border: ’2px dashed #999′
    });
    Maybe the border properties can be converted to a setting field as well.

  • bob

    Hi,
    thanks for this really great tut.

    Is there an updated version with jquery 1.4 by any chance?

  • http://- Aneta Martens

    Would it make sense to offer a working full version of iNettuts with all the positive changes as a download?

    I would be very happy if someone takes the trouble, and published it.

    Most users like me can not program the so well on the grounds it would be beneficial to the entire community.

    Here is a good example is this:
    http://www.v-mpf.jp/mode/

    Sincerely,
    Aneta Martens

    • http://verbdesign.com Jason Dinsdale

      Great work Aneta!It makes total sense to include cookie enabling.
      Aneta – a question about restoring the original layout. Have you managed to get this working? The ‘Click here’ link to restore items does work, well at least not in Firefox 3.6 on a Mac any how??

      A good question from Bob, is there a workaround for jQuery 1.4 yet?

      Fabulous work, a really great tutorial. I am working on a homepage to include the drag and drop panel functionality. I will post a link when its up the scratch! I’d love to get your feedback.

      Thanks again.

      Jas

  • Pingback: » Créez une page de widgets inspirée de iGoogle at Path-Tech - Les news les plus vitaminées du web

  • iamadesigner

    hi,
    That s excellent tutorial.. Is this an open source. can i use it on production sites

  • John

    Not Work jQuery 1.4.2, But thanks to you!

  • michael

    couple of things:
    * also like to update to jquery 1.4.2 if possible

    and
    * if one column is set to 100% the next to 70% the next to 30% functionality works to move the layouts around which is awesome. However if you pick up one of the widgets in the 70% box and try to move it to the 30% box the right side of the widget hits the outside of the browser.

    This causes the widget to not be movable into that 30% box.

    Is it possible to have the widgets disappear off the side of the screen if moved over to the edges of the browser to fix the above issue?

  • Seth

    Has anyone gotten this compatible with jQuery 1.4.2 and jQuery UI 1.8 ? Really need this to work with those libraries.

    Thanks.

  • Cassiano

    Nice plugin this one, James! :D

    I found it yesterday but, reading the last comments, i realized it wouldn’t work with latest versions of jQuery and jQuery UI…. Si i basically made from scratch”, which was, i must say, pretty easy.

    To whoever is needing it or wanna take a look on another solution, check above:

    First, with Portlet Sortable jQuery UI plugin you wont need anythingelse, basicaly: http://jqueryui.com/demos/sortable/portlets.html . You can check its API here: http://jqueryui.com/demos/sortable/

    After doing it work, you can use the Update and Stop events of Sortable and send the order to users DB prefs or a cookie.

    The code is simple as that:

    $(function() {
    var auxOrdem = “”;

    $(“.containerBoxesHome”).sortable({
    //options of your sortable

    update: function() {
    auxOrdem = “”;
    //.containerBoxesHome is the class of each col on the grid; as im using .each() jQuery function
    //you’ll be able to have more or less then 3 columns
    $(“.containerBoxesHome”).each(function() {
    //toArray is a Sortable event to output a col (ul or div) with his childrens ordered. You can
    //use serialize (serializable) too
    auxOrdem += $(this).sortable(“toArray”)+”_”;
    });

    },

    stop: function() {
    auxOrdem = auxOrdem.substr(0,auxOrdem.length – 1);
    //make an ajax requisition to the above script by passing a string with all portlets ordered and well
    //separated
    $.get(“../includes/scripts/salva_ordem_boxes.php”, { ordem: auxOrdem.replace(/,/g,’-') } );
    }
    });

    Pros of this is the possibility of using jQuery and jQueryUI in later versions and, of course, it’s WAY lightweighted.
    About the close and edit portlets, it can be easily implemented too.

    Cheers

  • divya

    first a login page is displayed to the user.user is log in and customize his page by changing drag drop the widget in columns.please help me

  • dgp

    Hello,

    Has anyone succeeded to implement the “add widget” functionality?

    Thanks alot?

  • Marcel Portela

    Nice implementation of portlets!
    And i’m would glad to anounce that i implemented this code with latest Jquery UI – 1.8.1 and jquery 1.4.2 without any modification.

    Just apply the “patch” from “Jon” comment (on March 10, 2010 at 8:10 am) to fix an small bug…

    And it works… nice and smooth….
    Congratulations!

    • Rhys

      Hi Marcel,
      Did you find that applying this patch meant that the cookie persistence breaks?

      I’m using jquery 1.4.2 and applying this patch restored the drag&drop but now the cookie seems to have stopped working.

  • Jose

    Any chance you can update the code to work with jQuery 1.4.2 and jQuery UI 1.8 ?

    • George OFI-4-

      I implemented it, too for latest Jquery UI (jquery-ui-1.8.custom.min.js) and (jquery-1.4.2.js). In file inettuts.js, use full path in line 33 and in line 142 use var notSortable = null;

      • Jose

        What line is that exactly ? My line 33 is “this.addWidgetControls();” which is part of the init function.

        BTW i can confirm that IE7/8 bug you mention with the Edit button.

      • Rhys

        Hi Jose,
        Were you able to work out what line George OFI-4- was referring to?
        Not line 33…

  • Pingback: BEST AJAX EXAMPLES - Nagpur

  • George OFI-4

    There is bug in IE7 and IE8 in widget’s head collapse button – If you press it twice, it shows the edit button….

    • Derek

      The IE collapse toggle bug is an easy one, you just have to spell out where you want the sprite img to be vs. allowing IE to “remember” its last position.

      change

      $(this).css({backgroundPosition: ”})

      to be

      $(this).css({backgroundPosition: ‘-52px 0′})

  • Asanka

    this is great effort……………………!!!
    but how use the Cookies????????? i want a explain about it…

  • Asanka Madhuranga

    this is good example,
    is it use Cookies????????????
    then where?

  • http://tunps.com tunpishuang

    if there must have at least one element for the widgetIndividual?
    how could i just make every widgets draggable and droppable?
    when i remove id “intro” , every widgets didn’t work.

  • spotter

    way too genius man

  • Pingback: como criar Portal estilo iGoogle « RN INFO

  • Pingback: como criar Portal estilo iGoogle « RN INFO

  • Barry

    The demo works great on Safari on Mac OS X, but I cannot drag the widgets around on Safari on the iPad (the entire page drags instead, which I believe is controlled by Safari itself).

    The “Edit”, “Close Edit”, minimize, maximize and delete all work on the iPad.

    Has anyone done any research onto extending this to work with the iPad?

  • Diwakar

    It’s really great!! But How do I get the widgets to exchange their positions? I want that the widget being displaced should take up the position of the widget its being replaced by. Please Help.

  • Maciek

    Awesome job! The effect is really neat.

    Could someone elaborate how to make some of the widgets wider e.g. the “gallery” box spanning TWO columns (height untouched, just the width)? or an extra-wide box spanning three columns? (What im asking about is a “snap to grid behaviour” with widgets sized using some relative unit, { 1×1 , 2×1, 3×1 } one unit high, 1-3 units wide, { 1×2, 2×2, 3×2 } two units high etc, u surely get the idea :))

  • Pingback: Drag And Drop Everything With 30 Handy jQuery Plugins | TechFleck

  • Pingback: Drag And Drop Everything With 30 Handy jQuery Plugins » abdie.web.id

  • Pingback: Drag And Drop Everything With These 30 Handy jQuery Plugins

  • FoneticFreek

    Has anyone managed to get this incorporated into a PHP app so you can add widgets?

    Very cool.

  • Pingback: Drag And Drop Everything With 30 Handy jQuery Plugins | Afif Fattouh - Web Specialist

  • Zahar

    Hello,
    i try ti make a Content text editable like the title one. but i can not.
    Can some one help me on that Please.

    Thanks

  • catchit

    I’ve edited the code so additional widgets can be added. Using the latest version of jQuery gives a noticeable performance increase, and allowed me to do some cool stuff with a js login overlay.

    (I use logins to people don’t need to rely on a cookie)

    • nerik

      @catchit: can you post your customized code ? I’d be very interested in seeing your solution about adding additional widgets and js login overlay. Thanx in advance…

      @FonetikFreak: have you yet realized any php integration ? May we see anything about ? Thanx to you too…

  • Arnaud

    Google is your friend. You can ask him everything :)
    “inettuts add widget”: http://jsabino.wordpress.com/2010/05/05/a-complete-igoogle-like-interface-example-with-jquery/

    • nerik

      Thanks a lot!!!

  • Pingback: lifestream » Blog Archive » publishall 08/04/2010 - collecting everything I do online

  • Pingback: Dexterology » Create iGoogle like interface

  • Nine6

    I am getting this error when running the latest jQuery and UI

    Uncaught TypeError: Object # has no method ‘sortable’

    It is referring to the following line:

    $(settings.columns).sortable({

    I am not sure how to fix this but that is definitely the reason why only the dragging is not working. Anyone have any ideas or fixes?

  • Pingback: Blog de Pavel Tocto » How to Mimic the iGoogle Interface

  • Pingback: $15 JavaScript AJAX edit! urgent… will take an hour or under « CyberLancer

  • Pijush

    hi,

    the example was great..I was trying to maximize the widgets but somehow its not happening..
    can you show me some way

  • Ali

    Nice work! However I have an issue with this (& other similar drag/drop panel tutorials). I want to be able to drag items from one list into any of the panels & then be able to sort them. Trouble is there are a couple of bugs…

    Using the demo, I have a list of ‘items’ which I can drag into any one of the the widgets, and then sort them. This works fine. It’s only when I start toggling these widgets (showing/hiding), or moving their positions, that things start going weird.

    First of all imagine that there is only one column with widgets sitting on top of each other:

    Example 1

    An item (or items) can be dragged into (and sorted) ok. If I then move Widget 2 above Widget 1 I can’t drag anything into Widget 2. I don’t get any errors in Firebug. Just nothing happens.

    Example 2

    If I ‘hide’ the contents of Widget 2, I cannot drag anything into Widget 3, 4 etc (Widget 1 is ok). If I then ‘show’ Widget 2 again, I can drag into Widget 3, etc but not Widget 2…

    Is this some sort of drag/drop/sort nesting issue?

    • Mak

      I am getting problems also when there are a lot of widgets stacked in one column. It seems like there is a limit to where they can be dragged vertically. Anyone who can help? Thanks!

      • Mak

        Solved this. remove the containment and it’s working. :)

  • http://www.denizliotokiralama.co denizli oto kiralama

    good coe thanks

  • Pingback: links for 2010-09-07 « Webデザインのリンク集 Webデザインポータルサイト S5-Style

  • gaoyang

    I need you help,thank

    I’m glad to come to your site, see you inettuts very familiar with can help me solve a problem? I downloaded the save function with the type, he occasionally turned on by default all the labels you all to open, then click the button which can be folded, and then click it open. I now want to enter the page in which a label is folded up, and then click the button displayed content, to ask how changes should be. I do not understand that knowledge, if you can help me change it, then you can modify the results of the issue I am not very grateful I gaoyangsqvqv@tom.com. The last to wish good health, and prosperity!

  • http://www.roople.com Robert

    We’ve been able to expand on this tutorial and add some database interaction etc. Feel free to check it out, and open a support ticket on site if you have any questions. We’d be happy to share code snippets, or discuss improvements.

    • Matthew

      I’m interested in seeing the database interaction. Where do I find that or how can I contact someone for it?

      Thanks,
      Matthew

    • http://pbjots.blogspot.com Phoebe Bright

      Be very interested in the database interaction. Love the interface!

  • http://www.kelmetnamag.com مجلة كلمتنا

    Great ! thank you

  • http://www.wizard-enterprises.com Bob Stone

    Very cool.

    How do you make the widgets stick in their locations between sessions?

  • eddie

    Great. One Question I am struggling with: How can I add a new widget out of javascript. If I add a new child Element, it is not movable and the Header Tools (edit close etc.) were not available.

    Please help.

    • aPeg

      Any progress made on this, im looking to do the same thing