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
  • Juan Manuel Lemus

    There’re an error on the implementation. At the sample you create an element with id as INTRO. If this don’t exists the code will broken, also if you change it settings (I tried changed movable to TRUE and it broke). I track the error to jQuery library.

  • ikate

    First of all: great tutorial!!!

    However, I ran into some “strange” behaviour when not defining any widgetIndividual, that is, if we do not want any widget to have a personalised configuration. In that case, method makeSortable would crash.

    Made it work by changing

    return $(‘> li:not(‘ + notSortable + ‘)’, settings.columns);

    to

    return $(‘> li’ + (notSortable != “” ? ‘:not(‘ + notSortable + ‘)’ : ”), _settings.columns);

    Hope this helps someone!

  • Anthony

    Hello, thank you for this great tutorial.
    I have a question, how to crea a new function for create a “popup” after the fonction “remove”

    Thanks for your help

  • shiva

    Hi,
    Can any body tell me if we can save the widget settings settings to be saved anywhere (cookie) when we leave the page. i.e next time if i re load the same page. it should be exactly how it was before reload.

    If we can achieve this can any one please help me out

    Thanks in Advance
    Siva

  • atiquzzaman

    this is really really great tutorial,but i wonder is it possible to implement it on WordPress or any other cms,if it is possible add tutorial on that too.
    highly appreciated
    And thanks in advance.

  • kannankeril

    Rahul – I am using a ASP.NET master page, I have it working inside the content page. Post your email here if you still need help on this.
    —————–
    Simon – thank for the IE fix, your fix worked and I have confirmed the code works in IE9, Firefox7 &
    Safari 5
    —————–
    D-Squared – thanks for the collapse code, It will definitely save me some time.
    —————–
    Doow – Simon’s tip worked for me and I have the script working with jquery-1.6.2.min.js & jquery-ui-1.8.16.custom.min.js, in a ASP.NET 4.0 website and interacting with a SQL Server DB, no cookies. Can you elaborate on the nature of your problem?
    —————–
    Juan Manuel Lemus – I ran into the same problem you did. Will post a fix if I figure it out, hope you do the same.
    —————–
    Finally, thank you James for sharing. I owe you a big one! If you ever write a book, I’ll buy a copy…I promise :)

    • http://www.predictableirony.com Darth Gremlin

      I beleive I am having the same problem as doow. When I use jquery 1.6.2 and jquery-ui 1.8.16, both of which located on my own server. Everything works except that the widgets are not movable. And there’s something weird about the way IE9 handles the button file. When I load the page the buttons on the widgets look normal but when I collapse, then uncollapse the collapse button shows the first part of the edit button rather than the up arrow. That I can work around, but I really don’t know why my widgets don’t move anymore.

      Any help would be great.

    • Digital Friend

      How can I make it work in ASP .Net with a Master page?

  • Pingback: 15 Best jQuery Drag And Drop Plugins | ZoomZum

  • nicolas

    hi, how i can start all the Widget collapsed???

    thx for the help

  • Pingback: How to make content of a widget scrollable? | SeekPHP.com

  • Pingback: Javascript menu for the HTML programming | SeekPHP.com

  • wergeld

    Testing this out and able to using:
    jquery-1.6.2
    jquery-ui-1.8.16 (I opted for the full install of all modules on the jQueryUI download page)

    Moving and collapsing work fine – however the “close”, “edit” and “collapse” images do not appear. When I do click on “edit” I am only able to change the title of the widgets (where available). I am not able to change the color of the widget (no color picker boxes show up).
    This occurs on IE8, IE9 and FF7/8.

    If I roll back to the default jQuery and jQueryUI versions then the images appear and I am able to edit the colors.

  • adas

    Hello,

    I think this is a great tutorial.

    I have tried making yellow, red and white only be placed in one spot, but when I try the code below to make them not movable, removable, Collapsible or Editable, they still show up and I can move edit and remove them.

    widgetIndividual : {
    intro : {
    movable: false,
    removable: false,
    collapsible: false,
    editable:false
    },
    gallery : {
    colorClasses : ['yellow','red','white']
    }
    }
    }

    Can you tell me how I can fix this?

    Thank you

  • http://pureehosting.com/?u=aHR0cDovL2V6aW5lYXJ0aWNsZXMuY29tLz9DaGVjay1PdXQtVGhlc2UtUmVzdW1lLU9iamVjdGl2ZS1TYW1wbGVzJmlkPTUwODc4MzA= Information Technology click here

    I feel this is among the so much important information for me. And i’m happy studying your article. However should statement on some general issues, The website style is perfect, the articles is truly excellent : D. Excellent activity, cheers

  • Bas

    If you have trouble in IE (COLLAPSE, CLOSE, EDIT shown instead of the images) include the inettuts.js.css stylesheet in the HTML:

    iNettuts – Welcome!

  • Aaron

    Hi,

    I was looking to be able to add and remove new widgets, this is a js cheat that I came up with.

    $(document).ready(function(){
    //Frontline

    $(‘#Frontline’).click(function(){
    $(“#widget-FL”).show();
    $.cookie(‘display’, ‘shown’);
    });
    $(‘.close’).click(function(){
    $(“#widget-FL”).hide();
    $.cookie(‘display’, ‘hidden’);
    });
    var display = $.cookie(‘display’);

    // Set the user’s selection for the left column
    if (display == ‘shown’) {
    $(“#widget-FL”).show();
    };
    // Set the user’s selection for the right column
    if (display == ‘hidden’) {
    $(“#widget-FB”).hide();
    };

    $(‘#Facebook’).click(function(){
    $(“#widget-FB”).show();
    $.cookie(‘display’, ‘shown’);
    });
    $(‘.close’).click(function(){
    $(“#widget-FB”).hide();
    $.cookie(‘display’, ‘hidden’);
    });
    var display = $.cookie(‘display’);

    // Set the user’s selection for the left column
    if (display == ‘shown’) {
    $(“#widget-FB”).show();
    };
    // Set the user’s selection for the right column
    if (display == ‘hidden’) {
    $(“#widget-FB”).hide();
    };

    });
    .

    and the html

    Add widget: Frontline | Frontline |

    I changed the Li id to id=”widget-FL” and id=”widget-FB”, you can name them whatever you want just keep copying the code. I am having issues with the cookie saving, so if anyone has a better way of doing this and can fix it let me know.

    Thank you,

    Aaron

  • http://www.virtualidstudios.com David

    Haha! I finally figured it out!

    How to add another widget:
    Ok, so I admit, it’s definitely a hack and not the most elegant form, but it works. For the life of me, I could figure everything else out, but it seemed as though the drag and drop needed to have all the widgets defined at once.

    So I simply saved the html to a variable, deleted everything, pasted it back in, added the widget with html and ran the function again.
    Replace ‘body’ with a container div so you don’t replace the entire page. So that you can test it on the current demo, ‘body’ was about the only container to work with. The below code will add another widget to column 2. Open the Demo, paste the below in firebug, and watch the magic unfold!

    // The code for your new widget here.
    $(‘#column2′).append(‘<li class="widget color-yellow"> <div class="widget-head"><h3>Widget title</h3></div><div class="widget-content"><p>Content of widget goes here</p></div></li>’);

    // Remove the "edit" "x", stuff added in the methods of iNettuts
    var remove = [$('.edit'), $('.remove'), $('.edit-box'), $('.collapse')];

    max=remove.length;
    for(i=0;i<max;i++){
    remove[i].remove();
    }

    var data = $(‘#columns’);
    $(‘#columns’).remove();
    $(‘body’).html($(data));

    iNettuts.init();

    • bob

      can u tel me where exactly i should paste it, i tryd in firebug but it is not happening..

  • Pingback: Kỹ thuật lập trình .com » Jquery Effect » 55 Jquery Tutorials, Resources, Tips And Tricks: Ultimate Collection

  • Yatish

    Hello,

    How I can integrate http://flowplayer.org/tools/demos/scrollable/vertical.html this sample in to this demo?

    please add sample for this.

    Thank you
    Yatish

  • http://www.akhbar-today.com أخبار اليوم

    Great , i would like to make it sure

  • http://yinhe12.com jiy

    when i update jquery to 1.7.1 ,it will report an error like this:

    Syntax error, unrecognized expression: )
    throw new Error( “Syntax error, unrecognized expression: ” + msg );

    i only change the jquery like this:

    this is why?

  • Pingback: jQuery 仿iGoogle 主页模块拖动编辑 | 银河十二区

  • http://jquery4u.com Sam

    This is the fix for jQuery 1.7.1 and jQuery UI 1.8.16.

    comment (or remove) this line with the one below.
    // return $(‘> li:not(‘ + notSortable + ‘)’, settings.columns);
    return $(‘> li’ + (notSortable != ” ? ‘:not(‘ + notSortable + ‘)’ : “”), settings.columns);

    Hope this helps someone.

    Thanks,
    Sam

  • http://camchat.camsex-chats.net Camchat

    Attractive element of content. I just stumbled upon your site and in accession capital to say that I get in fact enjoyed account your weblog posts. Any way I’ll be subscribing in your feeds and even I success you get admission to constantly rapidly.

  • Pingback: 10 jQuery Drag and Drops | jQuery4u

  • Jam

    Has anyone come across working with settings page for all the widgets where i can select the widgets and i could display only selected and minimum should be say 8 Widgets and i have 12 widgets all together among which i can select.

    Any one please help me on this… as im scratching my head on this…:(

  • dream

    Can i include this code under php code.I have run this code under php file it looses the draggable property.like
    function abc()
    {
    …..
    ….
    $result=”";
    ……

    }
    Please give me the solution.

    • faris

      add the to the top of the javascript file and end it with tag .. for any php content in between shall be placed within .. i hope it was clear enough .

      • faris

        sorry , apparently nettuts removes tags .. ok , place % at the top of the js file and end it with %% .. remove the % .. place the php code within php tags

  • http://www.tfk-kos.ru/strelyalki.html?start=32 тут зайцев нет

    It is in reality a nice and useful piece of information. I’m satisfied that you shared this useful information with us. Please stay us up to date like this. Thank you for sharing.

  • dream

    Pls if anyone kno the solution of my problm….when put this code under php.its draggabl proprty missed.whyyyyyy??????
    pls reply..

    • Sandeep Gill

      Hi Dream,

      I ran into the same problem and it was because of the order of script inclusion. What order did you include the scripts in your page.

      Include them in the same order as they have been in the resource folder and it should be fine, I have tried the same in my php page and it worked fine.

  • fubourius

    not working for ie explorer right?

    • Sameer

      Whether this will work in IE9 or not? Please confirm.

  • Shaun

    Hi,

    I’m attempting to drag and drop between the Portlets i,e, taking draggable content from Portlet and then drop it onto another. Very annoyed with myself as I solved this problem some time ago and can’t find my code which has taught me a valuable lesson for the future! Do remember that the drop target had to be recalculated as it could have been moved.

    Any help much appreciated, think the issue is with mouse event bubbling, happy to share the result when I’ve got it working.

    Many thanks,

    Shaun

  • Rob

    I am dynamically adding widgets to the page using AJAX. I am able to run the init() functions just fine (adding the close, edit, and minimize), but I cannot for the life of me get the draggable functionality working again. Any tips?

  • Pingback: I Google Tarzı Taşınabilir Div’ler

  • ham

    How can I add the functionality so users can maximize the Widgets and then Manimize it back to the original size? Thank you for the great tool and your help

  • sameer

    not working with IE9? can you please confirm.

  • Roi

    Great script!
    Can you please upload the PHP part that save data in DB ?

  • http://world-of-programmer.blogspot.com Angga

    anyway I’m looking for a dragable widget like that, and I found one here..
    so far so good..
    thanks..

  • wwredback

    Awesome!!

    Can you please upload the PHP part that saves the data in DB, so it becomes more of a CMS?

    Thanks

  • Bhavesh

    It is not working in IE9.. what is the problem??….

  • Pingback: 44 Excellent jQuery Tutorials For Web Developers

  • varsha

    very cool, bt not working with ie9, plz help

    • swapnil

      put in your header before any element like below.

  • http://www.francescopepe.com Francesco

    It’s possible to resize the panel?

  • Ananth

    Can i resize the widget,

  • Mani Rathinam

    its just an awesome design ! i am developing a similar one for my project but i need the customized format ie the sorted format, to be saved for an particular user ! can i save the changes ? plssssssssssss help ! thanks in advance !

  • Pingback: Extensive Collection Of jQuery Drag And Drop Plugins | CS5 Design

  • Pingback: Lots of JQuery Tutotials to Help You Customize Your Site | #1 Design Utopia Trend

  • Vivek

    Is there a way to get this working on an iOS device?

    • aroth

      Yes, it’s called Mobile Safari. Beyond that; if you’re making an app you should prefer using native UI components, not embedded web views.

  • Pingback: Extensive Collection Of jQuery Drag And Drop Plugins | TEKNQ

  • Lakshmi

    Hello…

    The article is very good and it is very helpful.

    I have a scenario where in I have to add the widgets dynamically… for which I tried the below code.
    All the functionality are working fine except the widget cannot be dragged.

    var WidName = “Widget 1″;

    var NewWidget = “” + WidName + “Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam magna sem, fringilla in, commodo a, rutrum ut, massa. Donec id nibh eu dui auctor tempor. Morbi laoreet eleifend dolor. Suspendisse pede odio, accumsan vitae, auctor non, suscipit at, ipsum. Cras varius sapien vel lectus.”;

    document.getElementById(“column1″).innerHTML += NewWidget;

    WidgetControl.init();

    Please help me out in solving the problem.

    Thank you in advance

  • Pingback: Extensive Collection Of jQuery Drag And Drop Plugins | Web Help 101

  • Yasir

    i put a variable in place of document in containment and call it elm. i change the elm value on mousedown. i alert the value of elm in end of the function it shows the value correctly but at the place where i need it i.e. at containment : elm it doesnt effect the value of elm there.

    $sortableItems.find(settings.handleSelector)
    .mousedown(function(e){
    elm=’parent’;
    //iNettuts.alwd_check( $(this).parent().attr(‘id’) );
    //alert($(this).parent().attr(‘id’));
    me = this;
    oldhtml= $(‘#container’).clone()
    index1 = $(“div”).index(me);
    var preWidth;
    preWidth = $(this).width();
    if (preWidth > 100){ $(this).parent().css({width: ”}); }
    else{ $(this).parent().css({width: ’50%’}); }
    prvidx = $(‘li’).index($(this).parent());
    }).mouseup(function () {
    if(!$(this).parent().hasClass(‘dragging’)){
    $(this).parent().css({width:”});
    }else {
    $(settings.container).sortable(‘disable’);
    $(this).parent().css({width:’50%’});
    }
    if($(this).parents(‘ul’).children().length == 1) evn = 0;
    //alwd_check( $(me).parent().attr(‘id’), $(‘.widget-placeholder’).parent().attr(‘id’) );
    prvcol=$(me).parent().parent();
    crtcol=$(‘.widget-placeholder’).parent();
    });

    $(settings.container).sortable({
    items: $sortableItems,
    connectWith: $(settings.container),
    handle: settings.handleSelector,
    placeholder: ‘widget-placeholder’,
    forcePlaceholderSize: true,
    revert: 250, delay: 0, opacity: .2,
    containment : elm,

    start: function (e,ui) {
    alert(elm);
    $(ui.helper).addClass(‘dragging’);
    $(ui.item).css({width:’25%’});
    },
    stop: function (e,ui) {
    $(ui.item).css({width:”}).removeClass(‘dragging’);
    $(settings.container).sortable(‘enable’);
    $(ui.item).css({opacity:”});
    $(‘.widget-head’).each(function () {
    $(this).css({width: $(this).parent().width()});
    $(this).css({height: $(this).parent().height()});
    $(this).css({‘z-index’: ’100′});

    $(‘.widget’).removeAttr(‘style’);
    index2=$(“div”).index(me);

    });