All posts tagged Drag & Drop

iGoogle UI for SharePoint – Part Three: Saving WebPart states using Cookies

Series Content

  1.     Part One – Overview, Concept,  HTML Structure & jQuery Basics
  2.     Part Two – Dragging, Dropping,  Sorting and Collapsing
  3.     Part Three – Saving WebPart states using Cookies – Current Article
  4.     Part Four – Control Adapters
  5.     Part Five – SharePoint 2010 Integration
  6.     Part Six – Bringing it all together
  7.     Bonus – Saving WebPart States using the Client Object Model

Overview

In Part Three we will take the code from our previous post and enable the page to remember the state of the various settings that we have applied such as the WebPart positions, the order, and minimise states and the webpart header colours.

Javascript

For this post we will require a new javascript library to help manage the Cookies stored for each user.  This library has been already included at the bottom of the existing demo’s.  This time however we will finally use it.  This Post will contain a lot of new Javascript and I will try my best to explain all of the items.

Saving the Position

The First thing that we will do is to Save the Position of all of the widgets at different times in the page life-cycle.  The first thing we need to do is dynamically set the Zones setting previously defined in previous posts. as well as add another setting into the definition to store the Cookie settings.

    settings : {
        columns : '.column',
        zones : '',
        widgetSelector: '.widget',
        handleSelector: '.widget-head',
        contentSelector: '.widget-content',
        editSelector: '.widget-edit',
        toolboxSelector: '#toolbox',
        widgetPlaceholder: 'widget-placeholder',
        badgeSelector: '.badge',
        positionCookie: 'widgetPositionsCookie'  
    },

This parameter was left blank on purpose to give the script the maximum flexibility.  So to find out where the widgets are we need to find out what “zones” that the widgets are occupying.  To do this we will create a new function called “defineZones” which will be used to iterate through the DOM and create a comma separated list of zones and their ID’s.  Now a Zone in this tutorial is the ID associated with each column.

		<div class='column' id='left'>
			<!-- Widgets Go here -->
		</div>		

		<div class='column' id='middle'>
			<!-- Widgets Go here -->
		</div>		

		<div class='column' id='right'>
			<!-- Widgets Go here -->
		</div>

The full function is shown below. We will then dive into the details of the code.

	defineZones : function () {
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			var temp = "";
			$j(settings.columns).each(function () {
				temp += "#" + $j(this).attr('id') + ",";
			});

			settings.zones = temp.substring(0, temp.length-1);
	},

Lines 6 – 9 goes through all of the columns defined in the settings.columns property and then creates the string based on the current items ID.  In line 11 i then set the dynamic string to the zones parameter only after trimming the end of the string again.

Now that we have a set of defined zones we need to now set up a new function to save the state of all the widgets contained with each column.

function SaveState()
{
    //Place SharePoint in No Conflict Mode
    var $j = jQuery.noConflict();
    //Update the Badge
    updateBadge();

    // Create/write a cookie and store it for 1 day
    setCookie(iSharePoint.settings.positionCookie, GetPositionState(), 1);  
}

function GetPositionState()
{    
    //Get a local array of the Zones
    var zones = iSharePoint.settings.zones.split(',');
    //Holder variable for widget Positions
    var widgetPositions = "";
    //Loop through each zone to create a pipe delimited string
    for(z in zones)
    {
        //Get the array of the current zone as CSV
        widgetPositions += $j(zones[z]).sortable('toArray') + "|";
    }
    //Trim the end off the string
    widgetPositions = widgetPositions.substring(0, widgetPositions.length-1);
    return widgetPositions;
}

This new function SaveState does the following actions.  To this method we will add first create a new reference to our global object and then we update the Badge Details in-case something new has changed.

We then create a new cookie with the setCookie() method which will use the positionCookie setting to store the cookie key and then call the GetPositionState() method which will return the positions as a comma separated & pipe separated list.

The GetPositionState() method firstly gets a local array of all of the zones we found using the previous method and add it into a new zone variable. Then we create a blank string value to store our positions.  Next we loop through each zone and for that we use the jQuery sortable methods with the “toArray” parameter which will get a comma seperated list of all of the zones and seperate each zone with a pipe character.  Finally we will create a javascript alert of the widgetPositions found.

Next we place the SaveState(); method call into the remove button event handler, as well as the stop method of the makeSortable method so that when we remove or move a widget on the page it will provide us with the new position.

At this stage we now can save the position of each widget when we close and move them on the page between columns.

Retrieving the Position & Order

The next step is to now to retrieve the position of the locations when you load the page so that the widgets appear in their previous positions.  To do this we need to firstly create a new method in our iSharePoint Namespace called loadStateFromCookie. This method will do two things.  Firstly it will load the position state from the cookie and place it into a variable, and then it will pass this to another method ProcessWidgetPositionData which will place the widgets in the correct locations.

	loadStateFromCookie : function() {
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			var PositionStates = getCookie(settings.positionCookie);	

			this.ProcessWidgetPositionData(PositionStates);		

			updateBadge();
	},

As you can see we firstly get a reference to the settings and the local jQuery object.  We then set a custom variable PositionStates to the cookie value by using the getCookie method.  We then pass this into the ProcessWidgetPositionData method and then finally we update the badge method which will ensure that the number of “closed” widgets is correct.  We will now dissect the processing function.

	ProcessWidgetPositionData :  function(PositionStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;
            // Configure cookie
            if (PositionStates) {
				//Get the Array of Orders
                var order = PositionStates.split('|');
				//Get the Array of Zones
				var zonearray = settings.zones.split(',');
				for(o in order)
				{
					//Position the boxes in the correct locations
					$j(settings.widgetSelector).each(function () {
						//Loops through each PositionState in order object
						if (order[o].search($j(this).attr('id')) != -1)
						{
							$j(zonearray[o]).append($j(this));
						}
					});
					//Reorder the boxes in each zone
					if (order[o] != "") {
						var _workOrder = order[o].split(',');
						for (i = 0; i < _workOrder.length; i++)
							{
								$j(zonearray[o]).append($j('#' + _workOrder[i]));
							}
					}
				}
            }
	},

This method is quite long but is easier than it appears.  As we have done for each method so far we need to get our local references to jQuery etc, and then we need to check that we have received some data in the PositionStates variable.  Next we need to split the array of values which we created in the SaveState() method back into an array so that we can loop through them.  We will also need to get the array of zones defined so that we can match up each of the arrays.

				//Get the Array of Orders
                                var order = PositionStates.split('|');
				//Get the Array of Zones
				var zonearray = settings.zones.split(',');

Next step is to loop through each of positions in the order array and perform two important tasks.  The first task is to place the correct widgets in their correct columns and then the next step is to ensure that they are in the correct order in that column.  We will firstly tackle the positions:

				for(o in order)
				{
					//Position the boxes in the correct locations
					$j(settings.widgetSelector).each(function () {
						//Loops through each PositionState in order object
						if (order[o].search($j(this).attr('id')) != -1)
						{
							$j(zonearray[o]).append($j(this));
						}
					});
                                //More code to come...

What this first part of the function does is loops through all of the widgets on the page when it loads and search if the current widget is found in the order variable (using the .search method, if an item is not found then it will return -1) then we want to add that item to the current column (defined in the zonearray).  NOTE: Because we are in control of the order that values are saved into the cookie the retrieval of the items can be predictable.

The next step is to then reorder the widgets in the current column.

					//Reorder the boxes in each zone
					if (order[o] != "") {
						var _workOrder = order[o].split(',');
						for (i = 0; i < _workOrder.length; i++)
							{
								$j(zonearray[o]).append($j('#' + _workOrder[i]));
							}
					}
				}

This will again ensure that there is a value in the current order value (there can be a column with no items) and then it will split the widget values into an array.   We now have an array of values which are in the order that they were saved so we can now process the array by appending the widgets in the current column again but this time in the correct order.  Simple. :)  The complete code for this method is shown below:

 

	ProcessWidgetPositionData :  function(PositionStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;
            // Configure cookie
            if (PositionStates) {
				//Get the Array of Orders
                var order = PositionStates.split('|');
				//Get the Array of Zones
				var zonearray = settings.zones.split(',');
				for(o in order)
				{
					//Position the boxes in the correct locations
					$j(settings.widgetSelector).each(function () {
						//Loops through each PositionState in order object
						if (order[o].search($j(this).attr('id')) != -1)
						{
							$j(zonearray[o]).append($j(this));
						}
					});
					//Reorder the boxes in each zone
					if (order[o] != "") {
						var _workOrder = order[o].split(',');
						for (i = 0; i < _workOrder.length; i++)
							{
								$j(zonearray[o]).append($j('#' + _workOrder[i]));
							}
					}
				}
            }
	},

Demo

To view a demo of where we have got to thus far click here

Saving the Collapsed State

The next step is to save the collapsed state of each of the widgets on the page.  The first step is to modify the SaveState(); function to include the following line at the end.

	setCookie(iSharePoint.settings.minimiseCookie, GetMinimisedState(), 1);

This line does what it say and sets the minimiseCookie to the value that is returned from the GetMinimisedState() method.  The GetMinimisedState() is a new method which is going to be added outside of the iSharePoint namespace and will simply go through each widget which has the minimised class attached to it and add it to an array.

function GetMinimisedState()
{
	var $j = jQuery.noConflict();
    var list = new Array();
    $j('.minimised').each(function () {
        list.push($j(this).attr('id'));
    });
    return list;
}

That’s it for saving the collapsed state ..nice and simple.

Retrieving the Collapsed State

To retrieve the collapse state is also another simple addition.  To do this we will load the state from the cookie set above and then for each of the widgets defined in the array it will set them to minimised and collapse the widget.

So the first step is to modify the loadStateFromCookie function to include two lines.  The first line is to set a local variable “MinimisedStates” to the cookie value:

var MinimiseStates = getCookie(settings.minimiseCookie);

the next step is then to process that data by passing it into a ProcessMinimiseData() method.

this.ProcessMinimiseData(MinimiseStates);

What this processing method does is split the array of MinimiseStates and then for each item in that array it will locate the widget with that ID.

	ProcessMinimiseData :  function(MinimiseStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;
		if (MinimiseStates != null) {
			MinimiseStates = MinimiseStates.split(',');

			for (i = 0; i < MinimiseStates.length; i++) {
				var id = "#" + MinimiseStates[i];
				$j(id).find(settings.contentSelector).slideToggle("slow");
				$j(id).toggleClass("minimised");
			}
		}
	},

If there is a widget in the array it will perform a slideToggle and then add the minimised class to the current widget.  The final step to this piece of the puzzle is to ensure that when you click on the button to minimise each widget is to run the save state method.  To do this we just modify the buttonFunctions method with the highlighted item below.

			$j('.collapse').mousedown(function (e) {
				e.stopPropagation();
			}).click(function () {
				$j(this).parents(settings.widgetSelector).find(settings.contentSelector).slideToggle("slow");
				$j(this).parents(settings.widgetSelector).toggleClass("minimised");
            //ADD THIS NEXT LINE IN
				SaveState();
				return false;
			})

That’s it for this section. :)

Demo

To view a demo of where we have got to thus far click here

Saving the Colour

This section we will save the current colour set in the webparts colour settings panel.  As we built in previous posts the colour widget can be any colour you can think of so we need to find a way to process this data on the fly.  Once again like above we need to modify the SaveState(); function to include another line at the end.

	setCookie(iSharePoint.settings.colorCookie, GetWidgetColor(), 1);

Like before this will set the cookie to the output from the GetWidetColor method.  This method simply goes through each widget and gets the value that is stored in the text box with the class iColorPicker.

function GetWidgetColor() {
	var $j = jQuery.noConflict();
	var list = new Array();
    $j(iSharePoint.settings.widgetSelector).each(function () {
        list.push($j(this).attr('id') + "|" + $j(this).find('.iColorPicker').val());
    });
	return list;
}

When the method loops through each text box it will also pipe delimit the widget id along with the colour so we are able to match them up on retrieval.  Next step..getting the colour back on load.

Retrieving the Colour

To retrieve the colour of the widget is very similar to the previous “retrieve” methods. To do this we will load the state from the cookie set above and then for each of the widgets defined in the array it will set them to colour defined and then re-color the widget.

So the first step is to modify the loadStateFromCookie function to include two lines.  The first line is to set a local variable “ColorStates” to the cookie value:

var ColorStates = getCookie(settings.colorCookie);

the next step is then to process that data by passing it into a ProcessWidgetColorData() method.

this.ProcessWidgetColorData(ColorStates);

What this processing method does is split the array of ColorStats and then for each item in that array it will find the widget with that ID using jQuery.Find() and set the Text Box Value, CSS and Handle Selector CSS.   The first couple of lines should be all too familiar by this point.  Below is the full method:

	ProcessWidgetColorData :  function(ColorStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;	

		if (ColorStates != null) {
			ColorStates = ColorStates.split(',');
			for (i = 0; i < ColorStates.length; i++) {

				var id = "#" + ColorStates[i].split('|')[0];
				var color = ColorStates[i].split('|')[1];
				$j(id).find('.iColorPicker').val(color);
				$j(id).find('.iColorPicker').css("background-color",color);
				$j(id).find(settings.handleSelector).css('backgroundColor', color);	

			}
		}
	},

That is all that is required for the colour modifications..we are almost done now :)

Demo

To view a demo of where we have got to thus far click here

Resetting Layout

The final step we need to implement for the base functionality is the ability for the user to “reset” his / her layout back to the predefined default.  To do this we need to add 5 lines of javascript into the buttonFunctions method into the reset button click event.

				deleteCookie(settings.positionCookie);
				deleteCookie(settings.minimiseCookie);
				deleteCookie(settings.colorCookie);
				$j('.iColorPicker').each(function () {
					$j(this).val('');
				});

All this does (its pretty obvious) is delete each of the user set cookies and loop through each colour picker text box and clear the values which will reset the colours.

Summary

So that’s the end of part 3 of this blog series.  It has been quite a long one but i hope that you have all learned something from this and can see how easy it is to combine jQuery and HTML to create a really great interface.  The full js for this post is shown below.  I will be hoping to get the next posts completed faster next time.  Please leave your comments..they are always appreciated.

var iSharePoint = {

    jQuery : $,

    settings : {
        columns : '.column',
		zones : '',
        widgetSelector: '.widget',
        handleSelector: '.widget-head',
        contentSelector: '.widget-content',
		editSelector: '.widget-edit',
		toolboxSelector: '#toolbox',
		widgetPlaceholder: 'widget-placeholder',
		badgeSelector: '.badge',
		positionCookie: 'widgetPositionsCookie',
		minimiseCookie: 'widgetMinimisedCookie',
		colorCookie: 'widgetColorCookie'
    },

    init : function () {
		settings = this.settings;
		this.defineZones();
		this.buttonFunctions();
		this.makeSortable();
		this.loadStateFromCookie();
		this.activateColours();
    },  

	defineZones : function () {
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			var temp = "";
			$j(settings.columns).each(function () {
				temp += "#" + $j(this).attr('id') + ",";
			});

			settings.zones = temp.substring(0, temp.length-1);

	},	

	loadStateFromCookie : function() {
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			var PositionStates = getCookie(settings.positionCookie);
			var MinimiseStates = getCookie(settings.minimiseCookie);
			var ColorStates = getCookie(settings.colorCookie);	

			this.ProcessWidgetPositionData(PositionStates);
			this.ProcessMinimiseData(MinimiseStates);
			this.ProcessWidgetColorData(ColorStates);

			updateBadge();
	},		

	ProcessWidgetPositionData :  function(PositionStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;
            // Configure cookie
            if (PositionStates) {
				//Get the Array of Orders
                var order = PositionStates.split('|');
				//Get the Array of Zones
				var zonearray = settings.zones.split(',');
				for(o in order)
				{
					//Position the boxes in the correct locations
					$j(settings.widgetSelector).each(function () {
						//Loops through each PositionState in order object
						if (order[o].search($j(this).attr('id')) != -1)
						{
							$j(zonearray[o]).append($j(this));
						}
					});
					//Reorder the boxes in each zone
					if (order[o] != "") {
						var _workOrder = order[o].split(',');
						for (i = 0; i < _workOrder.length; i++)
							{
								$j(zonearray[o]).append($j('#' + _workOrder[i]));
							}
					}
				}
            }
	},		

	ProcessMinimiseData :  function(MinimiseStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;
		if (MinimiseStates != null) {
			MinimiseStates = MinimiseStates.split(',');

			for (i = 0; i < MinimiseStates.length; i++) {
				var id = "#" + MinimiseStates[i];
				$j(id).find(settings.contentSelector).slideToggle("slow");
				$j(id).toggleClass("minimised");
			}
		}
	},

		ProcessWidgetColorData :  function(ColorStates){
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;	

		if (ColorStates != null) {
			ColorStates = ColorStates.split(',');
			for (i = 0; i < ColorStates.length; i++) {

				var id = "#" + ColorStates[i].split('|')[0];
				var color = ColorStates[i].split('|')[1];
				$j(id).find('.iColorPicker').val(color);
				$j(id).find('.iColorPicker').css("background-color",color);
				$j(id).find(settings.handleSelector).css('backgroundColor', color);	

			}
		}
	},	

	activateColours : function () {
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			$j(".pickerTable").click(function () {
				$j('.iColorPicker').each(function () {
					var str = "";
					str += $j(this).val();
					$j(this).parents(settings.widgetSelector).find(settings.handleSelector).css('backgroundColor', str);
				});
				SaveState();
			});
	},	

    makeSortable : function () {
		    var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			$j(settings.columns).sortable({
				connectWith: $j(settings.columns),
				handle: settings.handleSelector,
				cursor: 'move',
				revert: true,
				placeholder: settings.widgetPlaceholder,
				forcePlaceholderSize: true,
				revert: 300,
				delay: 100,
				opacity: 0.8,
				start: function (event, ui) {
					$j(settings.columns).css("background-color","#e7e5e5");
				},
				stop: function (event, ui) {
					$j(settings.columns).css("background-color","transparent");
					$j(settings.toolboxSelector).css("background-color","#fff");
					SaveState();
				}
			})		

    },		

	buttonFunctions : function() {
	        var iSharePoint = this,
            $j = jQuery.noConflict(),
            settings = this.settings;

			$j('.slide').mousedown(function (e) {
				// Stop event bubbling:
				e.stopPropagation();
			}).click(function () {
				//To Do - Slide toolbox down
				$j(settings.toolboxSelector).slideToggle("slow");
			// Return false, prevent default action:
			return false;
			}) 

			$j('.reset').mousedown(function (e) {
				// Stop event bubbling:
				e.stopPropagation();
			}).click(function () {
				if(confirm('Are you sure you want to reset your layout?')) {
				deleteCookie(settings.positionCookie);
				deleteCookie(settings.minimiseCookie);
				deleteCookie(settings.colorCookie);
				$j('.iColorPicker').each(function () {
					$j(this).val('');
				});
				location.reload();
				}
			// Return false, prevent default action:
			return false;
			}) 

			$j('.edit').mousedown(function (e) {
				e.stopPropagation();
			}).click(function () {
				//To Do - What to do when clicked first time
				$j(this).parents(settings.widgetSelector).find(settings.editSelector).slideToggle("slow");
				return false;
			})

			// Create new anchor element with class of 'remove':
			$j('.remove').mousedown(function (e) {
				// Stop event bubbling:
				e.stopPropagation();
			}).click(function () {
				// Confirm action - make sure that the user is sure:
				if(confirm('Please confirm the removal of the widget.  This can be restored from the widget toolbox at the bottom of the screen.')) {
					$j(this).parents(settings.widgetSelector).appendTo(settings.toolboxSelector);
					SaveState();
				}
			});

			$j('.collapse').mousedown(function (e) {
				e.stopPropagation();
			}).click(function () {
				$j(this).parents(settings.widgetSelector).find(settings.contentSelector).slideToggle("slow");
				$j(this).parents(settings.widgetSelector).toggleClass("minimised");
				SaveState();
				return false;
			})
	},
};

function updateBadge()
{
            $j = jQuery.noConflict(),
			$j(iSharePoint.settings.badgeSelector).text($j(iSharePoint.settings.toolboxSelector + " > " + iSharePoint.settings.widgetSelector).size());
}

function SaveState()
{
	//Place SharePoint in No Conflict Mode
	var $j = jQuery.noConflict();
	//Update the Badge
	updateBadge();

	// Create/write a cookie and store it for 1 day
	setCookie(iSharePoint.settings.positionCookie, GetPositionState(), 1);
	setCookie(iSharePoint.settings.minimiseCookie, GetMinimisedState(), 1);
	setCookie(iSharePoint.settings.colorCookie, GetWidgetColor(), 1);
}

function GetPositionState()
{
	//Get a local array of the Zones
	var zones = iSharePoint.settings.zones.split(',');
	//Holder variable for widget Positions
	var widgetPositions = "";
	//Loop through each zone to create a pipe delimited string
	for(z in zones)
	{
		//Get the array of the current zone as CSV
		widgetPositions += $j(zones[z]).sortable('toArray') + "|";
	}
	//Trim the end off the string
	widgetPositions = widgetPositions.substring(0, widgetPositions.length-1);
	return widgetPositions;
}

function GetMinimisedState()
{
	var $j = jQuery.noConflict();
    var list = new Array();
    $j('.minimised').each(function () {
        list.push($j(this).attr('id'));
    });
    return list;

}

function GetWidgetColor() {
	var $j = jQuery.noConflict();
	var list = new Array();
    $j(iSharePoint.settings.widgetSelector).each(function () {
        list.push($j(this).attr('id') + "|" + $j(this).find('.iColorPicker').val());
    });
	return list;
}

jQuery(document).ready(function(){
		var $j = jQuery.noConflict();
		$j(iSharePoint.settings.widgetSelector).corner();
		updateBadge();
		iSharePoint.init();	

});