﻿/// <reference path="jquery.intellisense.js"/>  
/**
 * Cobalt Elements for jQuery
 * ajaxgrid converts a standard grid to be handled fully by ajax.
 *
 * These methods are build on the jQuery and interface foundation to provider
 * client functionality to the Cobalt CMS system.
 *
 * Copyright (c) 2007 Scorpion Design, Inc.
 *
 */
(function($) {

	// Create the cobalt namespace if it has not already been done.
	$.cobalt = $.cobalt || {};

	// Combobox contructor.
	$.cobalt.ajaxgrid = function(el,o) { this.init(el,o); };

	// Return a new instance of the cobalt combobox.
	$.fn.ajaxgrid = function(o)
	{
		return this.each(function()
			{
				this.$ajaxgrid = new $.cobalt.ajaxgrid(this,o);
			});
	};

	// Static properties and methods.
	$.extend($.cobalt.ajaxgrid, {

		nbsp : String.fromCharCode(160),

		// Process any named formatting.
		formatting : {
		
				PhoneFormat : function(data)
					{
						// Remove any non-digit characters.
						var phone = data.replace(/\D/g,'');

						if (phone.length == 10)
							// A standard phone line.
							return "(" + phone.substr(0, 3) + ") " +
								phone.substr(3, 3) + "-" +
								phone.substr(6);
						else if (phone.length > 10)
							// Extra digits are probably an extension.
							return "(" + phone.substr(0, 3) + ") " +
								phone.substr(3, 3) + "-" +
								phone.substr(6, 4) + " x" +
								phone.substr(10);
						else if (phone.length > 3)
							// Otherwise return an unformatted number.
							return phone;
						else
							// If we have less than 3 digits, probably the source data was not a 
							// number at all, so return that information.
							return data;
					},

				Binary : function(data,format)
					{
						if ($.toBool(data))
							return format.Code||'';
						else
							return format.AltCode||'';
					},

				// Return the value replaced with asterisks for security.
				PasswordFormat : function(data,format)
					{
						return new Array((''+data).length).join('*');
					},

				DateTimeFormat : function(data,format)
					{
						if (!data)
							return '';
						else if (data.constructor!=Date)
							return data;
						else
						{
							format = format||{};
							if (!format.Code) format.Code = 'd-MMM-yyyy';
							return format.Code.replace($.dateTimeRegex,function()
								{
									return $.dateTimeProcess(data, Array.prototype.slice.call( arguments, 1 ));
								});
						}
					},
				
				Currency : function(data,format)
					{
						// Parse out the parts of the number.
						var digits = typeof(format.Digits)=='undefined' ? 2 : $.toInt(format.Digits);
						var amount = parseInt(Math.round($.toFloat(data)*Math.pow(10,digits))).toString();
						var left = amount.substr(0,amount.length-digits);
						if (left.length==0) left = '0';
						
						// Get the other digits
						var right;
						if (amount==0 && digits>0)
							right = (1*Math.pow(10,digits)).toString().substr(1,digits);
						else
							right = digits>0 ? amount.substr(amount.length-digits,digits) : '';

						// Build the number groupings.
						var number = [];
						while (left.length>0)
						{
							if (left.length>3)
							{
								number.unshift(left.substr(left.length-3,3));
								left = left.substr(0,left.length-3);
							}
							else
							{
								number.unshift(left);
								left = '';
							}
						}

						// Return the formatted number.
						return '$'+number.join(',')+(right?'.'+right:'');
					},
				
				PageMethod : function(data,format,row)
					{
						try { eval("data = "+format.Code+"(data,format,row);"); }
						catch(ex){;}
						
						return data;
					},
				
				Preview : function(data,format)
					{
						if (!data)
							return '';
						
						var clean = data.replace(/<\/?[^<]+>/g,'').replace(/&nbsp;/g,' ').replace(/\s+/,' ');
						var digits = $.toInt(format.Digits);
						if (!digits || clean.length < digits)
							return clean;
						else
							return clean.substring(0,digits) + ' ...';
					}
			},

		leadingZero : function(val)
			{
				if (val<10)
					return '0'+val;
				else
					return val;
			}

	});

	// Extend the combobox prototype with these shared properties/methods.
	$.extend($.cobalt.ajaxgrid.prototype, {

		r_trs : /<tr\b/gi,

		init : function(el,o)
			{
				$.extend(this,o||{});

				// Fire these events when the results are loaded.
				this.onResultsLoaded = [];

				// If we are paging more than 2000 results, don't bother setting it up as an ajax grid.
				if (this.Paging && this.TotalResults && this.TotalResults > 2000) return;
				
				// Extend the options to this object.
				this.element = $(el);
				this.id = this.element.attr('id');
				this.name = this.element.attr('name');
				this.DataSources = {};

				// If we don't have a defined item template, build the defaults.
				if (!this.ItemTemplate)
				{
					this.ItemTemplate = this.buildItemTemplate(this.DataClass||'data');
					this.AlternatingItemTemplate = this.buildItemTemplate(this.DataAltClass||'dataalt');

					// Count the number of rows for a single item.
					this.StandardRows = 0;
					var m;
					while (m=this.r_trs.exec(this.ItemTemplate))
						this.StandardRows++;
				}
				
				if (this.ItemTemplate.indexOf('System.Web.UI.ITemplate')>0)
					this.RowsFilter = '[icobalt=System.Web.UI.ITemplate][_item]';
				else
					this.RowsFilter = 'tr.'+(this.DataClass||'data')+',tr.'+(this.DataAltClass||'dataalt');

				// Initialize the paging.
				if (this.Paging)
					this.initPaging();

				// Initialize the search area.
				if (this.SearchProperties)
					this.initSearch();

				// Initialize the header sort.
				if (this.EnableHeaderSort)
					this.initSorting();

				// Get the data rows.
				var rows = this.getDataRows();

				// Look for and grab any display lists.
				this.initLists();

				// Capture the submit buttons for this grid.
				this.element
					.find('input:button,input:image,input:image')
					.bind('click',{grid:this},this.handleSearchButton);

				// For a CMS-based AjaxGrid, the insert point will be pre-defined.
				if (this.CMSTokens)
				{
					this.insertPoint = this.element.find("[_aplaceholder='true']");
				}
				else
				{
					// Get the insert point (the after the last data row of the table).
					var last = rows.filter(':last');
					this.insertPoint = last.next('tr');

					// If we don't have a row, search for the nospacing or footer row.
					if (!this.insertPoint.length)
					{
						// Newly grab all rows in the table.
						rows = this.element.children();
						if (rows.is('tbody')) rows = rows.children();

						// Get the last row of the table.
						last = rows.filter(':last');

						// Get the first nospacing or datafooter row.
						this.insertPoint = rows.filter(function(){ return $(this).is('tr.nospacing,tr.datafooter'); }).filter(':first');
					}
				}
				
				// If we STILL haven't found an insert point.
				if (!this.insertPoint.length)
				{
					// Create a new hidden row, and add it to the end of the table, use that as the insert point.
					this.insertPoint = $('<tr style="display:none"><td></td></tr>').insertAfter(last);
				}

				if (this.EditData)
				{
					// If we don't have an edit item template, build the default.
					if (!this.EditItemTemplate)
						this.EditItemTemplate = this.buildEditItemTemplate(this.EditData.DataEditClass||'data dataedit');

					// Initialize any editing modes.
					if (this.EditData.EditCell > 0)
						this.initEdit(rows);

					// If have a drag sell, set that up as well.
					if (this.EditData.DragCell>0)
						this.initDrag(rows);

					// Initialize any delete buttons.
					if (this.EditData.DeleteCell > 0)
						this.initDelete(rows);

					// Initialize any add row buttons.
					if (this.AjaxAdd)
						this.initAdd(rows);

					// Get the results, which will be needed for the edit.
					if (!this.checkResults)
					{
						this.checkResults = true;
						this.getResults();
					}

					// If we have an EditHeaderTemplate, check for and bind any full edit.
					if (this.EditHeaderTemplate)
						this.initFullEdit(rows);

					// Set the onEditItem event, as appropriate.
					if ($.isFunction(this.EditData.onEditItem))
						this.onEditItem = this.EditData.onEditItem;

					// Preload the save/cancel images.
					$.preload(this.EditData.SaveImage);
					$.preload(this.EditData.CancelImage);
				}
			},

		// Get the results of this table as a json object.
		getResults : function(data,callback)
			{
				// Get the folder list.
				var url = $.getAjaxUrl(window.location.href,{AjaxGrid:this.id,XYZPDQ:Math.random()});
				if (url)
				{
					// Define the options.
					var options = { url:url, dataType:'json' };
					
					// If we have postback data, set it.
					if (data)
					{
						options.type = 'POST';
						options.data = data;
						this.element.loading();
					}

					// If we have a callback, add it to the stack.
					if ($.isFunction(callback))
						this.onResultsLoaded.push(callback);

					// Build the success event.
					options.success = function(grid){
						return function(results){
							grid.element.doneLoading();
							grid.Results = results;
							var fn;
							while(fn=grid.onResultsLoaded.shift())
								if ($.isFunction(fn)) fn();
							if (grid.pendingAction)
								grid.renderResults();
						};
					}(this);
					
					// Fire the event.
					$.ajax(options);
				}
			},

		// Reload the grid contents.
		reload : function(callback)
			{
				this.element.loading();
				var fn = function(grid,callback){
					return function(){
						grid.filterResults();
						grid.renderResults();
						grid.updateSearch();

						// Once the grid is redrawn, handle any edit controls on the page as well.
						if (grid.EditData)
						{
							// Get the data rows.
							var rows = grid.getDataRows();

							// Initialize any editing modes.
							if (grid.AjaxEdit)
								grid.initEdit(rows);

							// Initialize any delete buttons.
							if (grid.AllowDelete)
								grid.initDelete(rows);

							// If we have an EditHeaderTemplate, check for and bind any full edit.
							if (grid.EditHeaderTemplate)
								grid.initFullEdit(rows);
						}

						// If we have a callback, fire it.
						if ($.isFunction(callback)) callback(grid);
					};
				}(this,callback);
				this.getResults(null,fn);
			},

		// Build the default item templates.
		buildItemTemplate : function(cls)
			{
				// Open the row.
				var sb = [];
				sb.push('<tr class="');
				sb.push(cls);
				sb.push('">');
				
				// Render each of the fields.
				var count = 0;
				var ccls;
				for (var p in this.FieldPropertyList)
				{
					// If there are custom classes defined for this cell, use them instead.
					if (this.ColumnClasses)
						ccls = this.ColumnClasses[count]||cls;

					sb.push('<td class="');
					sb.push(ccls);
					sb.push('"')
					var align = this.ColumnAlign[count];
					switch (align)
					{
						case "Left":
						case "Right":
						case "Center":
							sb.push(' align="'+align+'"');
							break;
					}
					sb.push('>[');
					sb.push(p);
					sb.push(']</td>');

					// Increment the count.
					count++;
				}
				
				// Close the row and return the template.
				sb.push('</tr>');
				return sb.join('');
			},

		// Build the default edit template.
		buildEditItemTemplate : function(cls)
			{
				// Open the row.
				var sb = [];
				sb.push('<tr class="');
				sb.push(cls);
				sb.push('">');
				
				// Render each of the fields.
				var count = 0;
				var ccls;
				for (var p in this.FieldPropertyList)
				{
					// Get the field data.
					var f = this.FieldPropertyList[p];
					var width = this.ColumnWidths[count];

					// If there are custom classes defined for this cell, use them instead.
					if (this.ColumnClasses)
						ccls = this.ColumnClasses[count]||cls;

					sb.push('<td class="');
					sb.push(ccls);
					sb.push('"')
					var align = this.ColumnAlign[count];
					switch (align)
					{
						case "Left":
						case "Right":
						case "Center":
							sb.push(' align="'+align+'"');
							break;
					}
					sb.push('>');

					if (count==this.EditData.DragCell-1 || (f.Name.length>=6 && f.Name.substring(0,6)=='_empty'))
					{
						// The drag cell is blank while editing.
						sb.push('&nbsp;');
					}
					else if (count==this.EditData.EditCell-1)
					{
						// The edit button is replaced by the save button.
						sb.push("<a href=\"javascript:void('Save')\">");
						if (f.Link && f.Link.ImageUrl)
						{
							sb.push('<img src="');
							sb.push(this.EditData.SaveImage);
							sb.push('">');
						}
						else
							sb.push('Save');
						sb.push('</a>');
					}
					else if (count==this.EditData.DeleteCell-1)
					{
						// The delete button is replaced by the cancel button.
						sb.push("<a href=\"javascript:void('Cancel')\">");
						if (f.Link && f.Link.ImageUrl)
						{
							sb.push('<img src="');
							sb.push(this.EditData.CancelImage);
							sb.push('">');
						}
						else
							sb.push('Cancel');
						sb.push('</a>');
					}
					else if (f.List && f.List.DataSource)
					{
						if ((f.Basic&&f.Basic.ControlType)=='DropDownList')
						{
							// If this control has a data source, render it as a select item.
							sb.push('<select name="');
							sb.push(p);
							if (width)
							{
								sb.push('" style="width:');
								sb.push(width-15);
								sb.push('px;');
							}
							sb.push('"></select>');
						}
						else
						{
							// These will get built dynamically.
							sb.push('[');
							sb.push(p);
							sb.push(']');
						}
					}
					else if (f.Image || f.Link)
					{
						// The drag cell is blank while editing.
						sb.push('&nbsp;');
					}
					else
					{
						switch(f.Basic && f.Basic.ControlType)
						{
							case 'CheckBox':
								sb.push('<input type="checkbox" name="');
								sb.push(p);
								sb.push('">');
								break;
							case 'Literal':
							case 'Label':
								sb.push('[');
								sb.push(p);
								sb.push(']');
								break;
							case 'TextArea':
								var height = f.Basic && f.Basic.Height;
								sb.push('<textarea name="');
								sb.push(p);
								if (width || height)
								{
									sb.push('" style="');
									if (width)
									{
										sb.push('width:');
										sb.push(width-15);
										sb.push('px;');
									}
									if (height)
									{
										sb.push('height:');
										sb.push(height);
										sb.push('px;');
									}
								}
								sb.push('"></textarea>');
								break;
							case 'PasswordBox':
								sb.push('<input type="password" name="');
								sb.push(p);
								if (width)
								{
									sb.push('" style="width:');
									sb.push(width-15);
									sb.push('px;');
								}
								sb.push('">');
								break;
							case 'PhoneControl':
								sb.push('<table name="'+p+'" class="nospacing" cellspacing="0" cellpadding="0" border="0">\
											<tr>\
												<td><input type="text" style="font:12px Arial;width:25px;" maxlength="3"/></td>\
												<td>-</td>\
												<td><input type="text" style="font:12px Arial;width:25px;" maxlength="3"/></td>\
												<td>-</td>\
												<td><input type="text" style="font:12px Arial;width:35px;" maxlength="4"/></td>\
											</tr>\
										</table>');
								break;
							default:
								var d = f.Format && f.Format.Type=="DateTimeFormat";
								var date = d && f.Format.Code.indexOf('yyyy') >= 0;
								var time = d && f.Format.Code.indexOf(':mm') >= 0;
								var icls = (date ? 'calendar' : '') + (time ? 'clock' : '');
								sb.push('<input type="text" name="');
								sb.push(p);
								if (icls)
								{
									sb.push('" class="');
									sb.push(icls);
									width -= 25;
								}
								if (width)
								{
									sb.push('" style="width:');
									sb.push(width-15);
									sb.push('px;');
								}
								sb.push('">');
								break;
						}
					}

					sb.push('</td>');

					// Increment the count.
					count++;
				}
				
				// Close the row and return the template.
				sb.push('</tr>');
				return sb.join('');
			},

		// Initialize the pading.
		initPaging : function()
			{
				// Update the paging.
				this.results = $('#'+this.id+'_ctl00_ResultsPerPage').bind('change',{grid:this},this.handleResults);
				var paging = this.element.find("tr:has(a[href*=_Next])");
				if (!paging.length)
				{
					// We may not have enough results to make a paging link, so look for it by the results found text.
					var trs = this.element.find("tr[icobalt='System.Web.UI.ITemplate']:not([_item])");
					for (var i=0;i<trs.length;i++)
					{
						var tr = trs.eq(i);
						var html = tr.html();
						if (/\d+\s+results\s+found/i.test(html))
						{
							paging = tr;
							break;
						}
					}
				}
				this.pagingSummary = paging.children('td:first-child');
				this.navigation = paging.children('td:last-child');
				this.navigation.find('a').bind('click',{grid:this},this.handleNav);

				if (!this.checkResults)
				{
					this.checkResults = true;
					this.getResults();
				}
			},

		handleResults : function(e){ e.data.grid.updateSearch(null,null,null,this.value); },
		handleNav : function(e){ e.data.grid.updateSearch(this.id.split('_').pop());return false; },

		// Initialize the search boxes.
		initSearch : function()
			{
				// Iterate through the search properties.
				for (var p in this.SearchProperties)
				{
					var s = this.SearchProperties[p];

					// Bind the search element.
					var el = $('#'+this.id+'_ctl00_'+p);
					
					if (el.is('input:text:not(.calendar)'))
					{
						el.bind('keyup',{grid:this},this.keySearch).onenter(function(){return false;});
					}
					else if (el.is('input:checkbox'))
					{
						el.bind('click',{grid:this},this.handleSearch);
					}
					else
					{
						// .NET may have rendered an onchange event -- cancel it.
						el[0].onchange = null;
						el.bind('change',{grid:this},this.handleSearch);
					}

					// Record a reference to the element.
					s.Element = el;
					el[0].search = s;

					// Get any optional data source information.
					if (s.DataSource)
						this.getDataSource(s,s.DataSource);
					else if (s.DynamicDataSource)
						this.getDataSource(s,'Dynamic:'+s.DynamicDataSource+'|'+(s.DynamicPrimaryKey+''));
				}

				if (!this.checkResults)
				{
					this.checkResults = true;
					this.getResults();
				}
			},

		// Handle the search events.
		handleSearch : function(e)
			{
				// Get the reference to the grid.
				var grid = this.handleSearch ? this : e&&e.data&&e.data.grid;
				if (!grid) return;

				// If this element had an autopostback, update any other lists.
				if (this.search && this.search.AutoPostBack)
					grid.updateSearchLists();

				// Private function to update the results.
				var fn = function()
				{
					grid.filterResults();
					grid.updateSearch();
					grid = null;
				}

				// If we need to do a new postback just for a change on this field.
				if (this.search && grid.SearchPostback && grid.SearchPostback[this.search.FieldName])
				{
					// Get the values of all search postback fields.
					var data = [];
					for (var p in grid.SearchPostback)
					{
						var s = grid.SearchProperties[p];
						data.push(s.Element.attr('name')+'='+$.encode(s.Element.val()));
					}

					// Call the get results method with a callback.
					grid.getResults(data.join('&'),fn);
				}
				else
					// Otherwise, just call it directly.
					fn();
			},

		handleSearchButton : function(e){ e.data.grid.updateSearch();return false; },
		keySearch : function(e)
			{
				// Only handle valid characters for this event.
				var key = e.which;
				if (key!=8 && key!=32 && key<=48) { return; }

				// If we already have a timeout scheduled, kill it.
				if (this.$timeout)
				{
					clearTimeout(this.$timeout);
					this.$timeout = null;
				}

				// Set a 1/5 second timeout to start the ajax call.
				var grid = e.data.grid;
				this.$timeout = setTimeout(function()
					{
						if (grid.filterResults()!=false)
							grid.updateSearch();
					},200);
			},

		// Get a remote data source.
		getDataSource : function(f,data,force)
			{
				// Translate the data source into a an id-safe value.
				if (!data) return;

				// Get the folder list.
				var url = $.getAjaxUrl(window.location.href,{AjaxList:$.encode(data)});
				if (force) url += '&R='+Math.random();

				if (url)
				{
					var list = f;
					$.ajax(
						{
							url:url,
							dataType:'json',
							success:function(results) { list.DataSource = results; }
						});
				}
			},

		// Initialize the sortable headers.
		initSorting : function()
			{
				this.sorting = this.element.find('td.dataheader a[id*=_Sort_]').bind('click',{grid:this},this.handleSort);

				if (!this.checkResults)
				{
					this.checkResults = true;
					this.getResults();
				}
			},

		// Handle a click on a sort field.
		handleSort : function(e)
			{
				// Remove any current sort indicators.
				e.data.grid.sorting.next('img').remove();
				
				// Get the sort criteria.
				var id = this.id.split('_');
				var sort = id.pop();
				var dir = sort.substr(sort.length-1,1);
				sort = sort.substr(0,sort.length-1)

				// Update the search criteria.
				e.data.grid.updateSearch(null,sort,dir);

				// Determine the sort direction the next time this item is clicked.
				var arrow;
				var link = $(this);
				switch (dir)
				{
					case "2":
						id.push(sort+'1');
						arrow = e.data.grid.DownArrow;
						break;
					default:
						id.push(sort+'2');
						arrow = e.data.grid.UpArrow;
						break;
				}

				// Re-assign the id attribute and add the arrow icon.
				link.attr('id',id.join('_')).after('<img src="'+arrow+'" border=0>');
				return false;
			},

		// If there are any display lists, grab those as well.
		initLists : function()
			{
				for (var p in this.FieldPropertyList)
				{
					// Get any optional data source information.
					var f = this.FieldPropertyList[p];
					if (f.List && f.List.DataSource)
						this.getDataSource(f.List,f.List.DataSource);
				}
			},

		// Get the rows of the table filtered by data and dataalt.
		getDataRows : function()
			{
				var filter = this.RowsFilter;
				if (filter.indexOf('[icobalt')==0)
					return this.element.find(filter);
				else
				{
					var rows = this.element.children();
					if (rows.is('tbody')) rows = rows.children();
					return rows.filter(function(){ return $(this).is(filter); });
				}
			},

		r_hrefId : /javascript:void\('\w+',(\d+)\)/i,

		// Get the rows belonging to a single item.
		getItemRows : function(item,id)
			{
				// Get the row this item is in as an array.
				var tr = item.parents('tr:first').attr('_id',id);
				var trs = [tr[0]];

				// If we have more than one row, we're going to assume that this line is in the first row.
				// This needs to be expanded upon later, but its a holding pattern for now.
				var rows = this.StandardRows;
				while (rows>1)
				{
					tr = tr.next().attr('_id',id);
					trs.push(tr[0]);
					rows--;
				}
				
				// Return the rows.
				return $(trs);
			},

		// Initialize the rows for editing.
		initEdit : function(rows)
			{
				var grid = this;
				rows.find("a[href^='javascript:void('][href*=Edit]").each(function(i){
					var link = $(this);
					var m = grid.r_hrefId.exec(link.attr('href'));
					if (m)
					{
						id = $.toInt(m[1]);
						link.data('grid',grid)
							.data('_id',id)
							.data('rows',grid.getItemRows(link,id))
							.click(grid.editItem);
					}
				});
			},

		// Initialize the rows for full editing.
		initFullEdit : function(rows)
			{
				var grid = this;
				grid.element.find("a[href^='javascript:void('][href*=Full]").data('grid',grid).click(grid.editFull);
			},

		// Initialize the drag handles.
		initDrag : function(rows)
			{
				var grid = this;

				// Now configure the drag buttons for the specified rows.
				if (rows)
					rows.find("a[href^='javascript:void('][href*=Drag]").each(function(i){
						var link = $(this);
						var m = grid.r_hrefId.exec(link.attr('href'));
						if (m)
						{
							id = $.toInt(m[1]);
							link.data('grid',grid)
								.data('_id',id)
								.data('rows',grid.getItemRows(link,id))
								.draggable(
									{
										helper:grid.dragHelper,
										containment:grid.element,
										start:grid.dragRow,
										drag:grid.draggingRow,
										stop:grid.dropRow,
										axis:'y',
										cursorAt:{left:0,top:10}
									});
						}
					});

				// If we have more than one drag element for the whole grid, we'll show the drag buttons.
				// Otherwise, we'll hide the drag buttons.
				var buttons = grid.element.find("a[href^='javascript:void('][href*=Drag]");
				buttons.each(function(i){
					var link = $(this);
					if (buttons.length>1 && link.data('_id')>0)
						link.css({visibility:'visible'});
					else
						link.css({visibility:'hidden'});
				});
			},

		// Return a drag helper, which is a div wrapping the drag icon.
		dragHelper : function(e)
			{
				var handle = $(this);
				var grid = handle.data('grid');
				return handle.clone().prependTo(grid.element.find('td:first'));
			},

		// Start dragging a row.
		dragRow : function(e,ui)
			{
				var handle = $(this);
				var currentid = handle.data('_id');
				var grid = handle.data('grid');
				var table = grid.element.absPosition();
				var element = handle.data('rows');
				
				var positions = [];
				var rows = grid.getDataRows();
				ui.helper.location = -1;
				var count = 0;
				var lastid = 0;
				for (var i=0;i<rows.length;i++)
				{
					var row = rows.eq(i);
					var pos = row.children('td:first').absPosition();
					var id = $.toInt(row.attr('_id'));
					if (id!=lastid)
					{
						// Record the position.
						pos.left = 0;
						pos.width = 2000;
						pos.height++;
						positions.push({pos:pos,frow:row,lrow:row});

						// Check to see if we're on the current row.
						if (id==currentid)
							ui.helper.location = count;

						// Update the counters.
						count++;
						lastid = id;
					}
					else if (positions.length>0)
					{
						// If we have more than one row with the same id, we'll just extend the hot zone vertically.
						var lpos = positions[positions.length-1];
						lpos.pos.height = pos.top-lpos.pos.top+pos.height+1;

						// And we note that we have a new last row.
						lpos.lrow = row;
					}
				}
				ui.helper.positions = positions;
				ui.helper.element = element;
				ui.helper.start = ui.helper.location;

				// Add the placeholder.
				ui.helper.placeholder = element
					.clone()
					.insertBefore(element.eq(0))
					.children('td:not(.rdrag)')
						.addClass('datadrag')
					.end();

				// Hide the element.
				element.hide();
			},

		// While dragging a row.
		draggingRow : function(e,ui)
			{
				// Get the position data.
				var loc = ui.helper.location;
				var len = ui.helper.positions.length;
				var left = ui.absolutePosition.left;
				var top = ui.absolutePosition.top;

				// Check for a matching position.
				for (var i=0;i<len;i++)
				{
					var p = ui.helper.positions[i];
					var pos = p.pos;
					if (left>pos.left && 
						left<(pos.left+pos.width) &&
						top>pos.top &&
						top<(pos.top+pos.height))
					{
						// If we've moved to a new zone.
						if (loc!=i)
						{
							// Move the placeholder.
							if (i>ui.helper.start)
								ui.helper.placeholder.insertAfter(p.lrow)
							else
								ui.helper.placeholder.insertBefore(p.frow)

							// Note the current location.
							ui.helper.location = i;
						}

						// No need to go any further, because we found a match.
						return;
					}
				}
			},

		// When a drag-and-drop item is dropped.
		dropRow : function (e,ui)
			{
				// Get the data about the row and its new location.
				var handle = $(this);
				var grid = handle.data('grid');
				var rows = ui.helper.element;
				var id = handle.data('_id');
				var loc = ui.helper.location;
				var p = ui.helper.positions[loc];
				var seq = (loc+1)*10+(grid.seqbase||0);
				if (loc > ui.helper.start)
				{
					ui.helper.element.insertAfter(p.lrow);
					seq += 5;
				}
				else if (loc < ui.helper.start)
				{
					ui.helper.element.insertBefore(p.frow)
					seq -= 5;
				}
				else
				{
					// We didn't move anything.
					ui.helper.placeholder.remove();
					ui.helper.element.show();
					return;
				}

				// Update the sequence.
				var field = grid.FieldNames[grid.EditData.DragCell-1];
				var url = $.getAjaxUrl(window.location.href,{AjaxEdit:grid.element.attr('id'),I:id,F:field,V:seq});
				if (url)
				{
					grid.element.loading(grid.timeout);
					var fn = function(grid){
						return function(){
							grid.element.doneLoading();
							grid=null;
						};
					}(grid);
					$.ajax({url:url,success:fn});
				}

				// Show the row in its new location.
				ui.helper.placeholder.remove();
				ui.helper.element.show();
				grid.resetRows();
			},

		// Reset the row.
		resetRows : function(rows)
			{
				// Get the rows and reset the colors.
				var grid = this;
				rows = rows || grid.getDataRows();
				var data = grid.DataClass||'data';
				var dataalt = grid.DataAltClass||'dataalt';
				if (data && dataalt && data!=dataalt)
				{
					rows.each(function(i) {
						var cls = Math.floor(i/(grid.StandardRows||1))%2?dataalt:data;
						$(this).attr('class',cls).children('td.'+data+',td.'+dataalt).attr('class',cls);
					});
				}
			},

		// Initialize the delete buttons.
		initDelete : function(rows)
			{
				var grid = this;
				rows.find("a[href^='javascript:void('][href*=Delete]").each(function(i){
					var link = $(this);
					
					// Make sure we have permissions to delete.
					if (!grid.AllowDelete)
					{
						link.hide();
						return;
					}
					
					// Set up the delete.
					var m = grid.r_hrefId.exec(link.attr('href'));
					if (m)
					{
						id = $.toInt(m[1]);
						link.data('grid',grid)
							.data('_id',id)
							.data('rows',grid.getItemRows(link,id))
							.click(grid.deleteItem);
					}
				});
			},

		// Initialize the add new row button.
		initAdd : function(rows)
			{
				// Get the row after which we'll insert the new row.
				var row = rows.eq(0).prev();
				if (!row.length)
					row = this.insertPoint.prev();

				this.element
					.find("a[href^='javascript:void('][href*=Add]")
						.data('grid',this)
						.data('rows',row)
						.data('_id',0)
						.click(this.editItem);
			},

		// Get the results row.
		getResultRow : function(id)
			{
				// If we don't have an id or any results, return a default row.
				if (!id || !this.Results)
					return this.getDefaults();
				
				// Look for a match.
				for (var i=0;i<this.Results.length;i++)
					if (this.Results[i][this.PrimaryKey]==id)
						return this.Results[i];

				// Return an empty row.
				return {};
			},

		// Update a results row with its new value.
		updateResultsRow : function(id,row)
			{
				// If we don't have an id or any results, do nothing.
				if (!id || !row)
					return this.getDefaults();

				// Look for a match.
				for (var i=0;i<this.Results.length;i++)
					if (this.Results[i][this.PrimaryKey]==id)
					{
						// If we found one, replace it.
						this.Results[i] = row;
						return;
					}

				// If we got this far, there was no match, so just add it to the end.
				this.Results.push(row);
			},

		r_panels : new RegExp('\\[PANEL:(No)?Has(\\w+)\\]([\\s\\S]*?)\\[/PANEL\\]','gi'),
		r_tokens : /\[(\w+)([^\]]+)?\]/g,
		r_cmstokens : /\{(\w+)(?:\:(\w+))?\}/g,

		// Convert a template string into proper HTML for rendering on the page.
		parseTemplate : function(template,data)
			{
				var grid = this;
				var html = template
						.replace(this.r_panels,function(m,m1,m2,m3)
							{
								var field = data[m2];
								var visible = (!m1 && field) || (m1 && !field);
								if (visible)
									return m3;
								else
									return '';
							})
						.replace(this.r_tokens,function(s,f)
							{
								return grid.format(f,data,s);
							});

				if (grid.CMSTokens)
				{
					return html
						.replace(this.r_cmstokens,function(s,f1,f2)
							{
								var f = f2||f1;
								var val = data && data[f];
								if (typeof(val)!='undefined')
								{
									// If we don't have any format types, return the value directly.
									if (!f2)
										return (val!=null && val.constructor==Date) ? $.dateTimeFormat(val) : val;

									// Check for an handle any formatting.
									switch (f1.toLowerCase())
									{
										case "http":
											return val.indexOf('http')!=0 ? 'http://'+val : val;
										case "www":
											if (val && val.indexOf('www')!=0 && val.split('.').length==2)
												return 'www.'+val;
											else
												return val;
										case "nohttp":
											return val.replace(/^https?:\/\//i,'');
										case "encode":
											return $.encode(val);
										case "decode":
											return $.decode(val);
										case "checked":
											return $.toBool(val) ? 'checked' : '';
										case "initial":
											if (val)
											{
												val = $.trim(val);
												if (val.length && val.indexOf('.')!=val.length-1)
													return val+'.';
												else
													return val;
											}
											else
												return val;
										case "random":
											var range = $.toInt(val);
											return range ? ''+parseInt(Math.random()*range) : '';
										default:
											return s;
									}
								}
								else
									return s;
							});
				}
				else
					return html;
			},

		// Edit all of the rows in the table at the same time.
		editFull : function(e)
			{
				// Get a reference to the current grid.
				var grid = this.editFull ? this : $(this).data('grid');

				// If we haven't yet gotten the results needed for editing, pause here for the loading to finish.
				if (!grid.Results)
				{
					grid.element.loading();
					var fn = function(grid){
						return function(){
							grid.editFull();
							grid = null;
						};
					}(grid);
					grid.onResultsLoaded.push(fn);
					return false;
				}

				// Hide the existing elements.
				var child = grid.element.children();
				if (child.is('tbody'))
					child.children().hide();
				else
					child.hide();
				
				// Get the first and last rows.
				var first = grid.Results.length ? grid.Results[0] : null;
				var last = grid.Results.legth ? grid.Results[grid.Results.length-1] : null;

				// Create the edit template.
				var sb = [];
				sb.push(grid.parseTemplate(grid.EditHeaderTemplate,first));
				sb.push(grid.parseTemplate(grid.EditFooterTemplate,last));
				grid.edittemplate = $(sb.join('')).appendTo(grid.element);

				// Find the placeholder.
				grid.editplaceholder = grid.edittemplate.find("[_aplaceholder='true']");
				grid.editrows = [];

				// Bind any save/cancel buttons in the header/footer.
				grid.edittemplate
					.find("a[href^='javascript:void('][href*=Save]")
						.data('grid',grid)
						.click(grid.saveFull)
					.end()
					.find("a[href^='javascript:void('][href*=Cancel]")
						.data('grid',grid)
						.click(grid.cancelFull)

				// Add each of the rows.
				for (var i=0;i<grid.Results.length;i++)
				{
					var data = grid.Results[i];
					var id = data[grid.PrimaryKey]||0;
					var html = grid.parseTemplate(grid.EditItemTemplate,data);
					var row = $(html).insertBefore(grid.editplaceholder).data('oldrow',data).data('_id',id);
					grid.bindEditRows(row,null,data,id,grid.saveFull,grid.cancelFull);
					grid.editrows.push(row);
				}

				// Fire the edit item event.
				grid.onEditItem(grid.element);
			},

		// Save the full edit.
		saveFull : function(e)
			{
				// Get a reference to the grid.
				var grid = this.saveFull ? this : $(this).data('grid');

				// Validate the form before going any further.
				if (!grid.validateAll())
					return false;

				var rows = [];
				for (var i=0;i<grid.editrows.length;i++)
				{
					// Get the next edit row.
					var editrow = grid.editrows[i];
					var oldrow = editrow.data('oldrow');
					var id = $.toInt(editrow.data('_id'));

					// Assign the values.
					var row = {};
					editrow.find('[name]').each(function(i){
						var input = $(this);
						var name = input.attr('name');
						if (input.is('input:checkbox'))
							val = input[0].checked;
						else if (input.is('table'))
						{
							// If this is a table with child inputs, add each of their values.
							val = '';
							input.find('input').each(function(i){
								val += $(this).val();
							});
						}
						else
							val = input.val();

						row[name] = val;
					});

					// Iterate through each of the properties in the new row.
					var fields = [];
					var values = [];
					for (var field in row)
					{
						// Get the old and new values.
						var oldval = oldrow[field];
						var newval = row[field];
						var changed;

						if (id==0 && newval==false)
							// If we have a new row with a checkbox that was NOT checked, it counts as a not changed.
							changed = false;
						else if ((typeof(oldval)=='undefined' || oldval==null) && newval!='')
							// If we didn't HAVE an old value, but we have a new value, then it HAS changed.
							changed = true;
						else
						{
							// Cast the value and compare.
							if (typeof(oldval)=='number')
								changed = $.toFloat(newval) != oldval;
							else if (typeof(oldval)=='boolean')
								changed = $.toBool(newval) != oldval;
							else if (newval.constructor==Date)
							{
								var val = $.cobalt.calendar.parseDate(newval,true);
								changed = (!val || val != oldval);
							}
							else
								changed = newval != oldval;
						}

						// If it changed, add the field and value.
						if (changed)
						{
							fields.push(field);
							values.push($.encode(val));
						}
					}

					//  If we have fields that changed.
					if (fields.length>0)
					{
						// If we have a totally new row, then we'll need to grab all the fields (not just the ones that changed).
						if (id==0)
						{
							var data = $.extend(oldrow,row);
							fields = [];
							values = [];
							for (var p in data)
							{
								var v = data[p];
								fields.push(p);
								values.push($.encode(v==null?'':v));
							}
						}

						// Add the row data.
						rows.push(id+","+$.encode(fields.join(","))+","+$.encode(values.join(",")));
					}
				}

				// If we have rows to update.
				if (rows.length)
				{
					// Get the url.
					var url = $.getAjaxUrl(window.location.href,{AjaxFullEdit:grid.element.attr('id')});
					if (url)
					{
						// Perform the ajax update.
						grid.element.loading(grid.timeout);
						$.ajax(
							{
								url:url,
								type:'POST',
								data:'Rows='+$.encode(rows.join(";")),
								dataType:'json',
								success:function(results)
									{
										// Kill the loading screen.
										grid.element.doneLoading();

										if (results && results.Error)
										{
											// Check to see if we have a control on the page to render the error message.
											var control = false;
											if (results.Error.Control)
											{
												for (var i=0;i<results.Error.Control.length;i++)
												{
													// If we find a control.
													var ctrl = grid.edittemplate.find("[id='"+results.Error.Control[i]+"']");
													if (ctrl.length)
													{
														// Set the message as appropriate and make it visible.
														if (ctrl.is(':visible') || $.trim(ctrl.html()||'').length==0)
															ctrl.html(results.Error.Message);
														ctrl.show();
														control = true;
													}
												}
											}

											// If we didn't find a control, alert the error message.
											if (!control && results.Error.Message)
												alert(results.Error.Message.replace(/<br>/gi,''));

											// Focus on an input element and null out the closure.
											var input = grid.edittemplate.find('input:text:first');
											setTimeout(function(){input.focus();},1);
											grid = null;
											return;
										}
										else
										{
											// Otherwise, close out the full edit and reload the grid.
											grid.cancelFull();
											grid.reload();
											grid.onSaveItem();
											grid = null;
										}
									}
							});
					}
				}
				else
					grid.cancelFull(e);
			},

		// Cancel the full edit.
		cancelFull : function(e)
			{
				// Get the grid.
				var grid = this.cancelFull ? this : e && e.data && e.data.grid ? e.data.grid : $(this).data('grid');

				// Remove the edit template.
				grid.editrows = null;
				if (grid.edittemplate)
				{
					grid.edittemplate.remove();
					grid.edittemplate = null;
				}

				// Show the original child elements.
				var child = grid.element.children();
				if (child.is('tbody'))
					child.children().show();
				else
					child.show();
			},

		// Edit a specific item.
		editItem : function(e)
			{
				// Get the basic variables.
				var el = $(this);
				var grid = el.data('grid');
				var oldrows = el.data('rows');
				var id = el.data('_id');

				// If we haven't yet gotten the results needed for editing, pause here for the loading to finish.
				if (!grid.Results)
				{
					grid.element.loading();
					var fn = function(el){
						return function(){
							el.trigger('click');
							el = null;
						};
					}(el);
					grid.onResultsLoaded.push(fn);
					return false;
				}

				var data = grid.getResultRow(id);

				// We have to have these two things.
				if (!grid || !oldrows)
					return;

				// Add the edit row just after the current row.
				var html = grid.parseTemplate(grid.EditItemTemplate,data);
				var editrows = $(html).insertAfter(oldrows.eq(oldrows.length-1));

				// If we have an id, we're not adding a new row, so hide the current row.
				if (id)
					oldrows.hide();

				// Bind the edit rows.
				grid.bindEditRows(editrows,oldrows,data,id,grid.saveEdit,grid.cancelEdit);

				// Fire the edit item event.
				grid.onEditItem(editrows,oldrows);

				// If we have an input element, set focus when the screen is finished redrawing.
				var input = editrows.find('input:text:first');
				if (input.length)
				{
					var fn = function(input){
						return function(){
							if (input.length) input[0].focus();
							input = null;
						};
					}(input);
					setTimeout(fn,1);
				}

				return false;
			},

		// Bind the edit row controls and events.
		bindEditRows : function(editrows,oldrows,data,id,onsave,oncancel)
			{
				var grid = this;

				// Assign the values.
				editrows.find('[name]').each(function(i){
					var input = $(this);
					var name = input.attr('name');
					var val = data[name]||'';

					// If this is a checkbox, set the value true/false.
					if (input.is('input:checkbox'))
					{
						val = $.toBool(val);
						if (id || val)
							input[0].checked = val;
					}
					else if (input.is('input.calendar'))
					{
						input.val($.dateTimeFormat(val,'MM/dd/yyyy'));
					}
					else if (input.is('input.calendarclock'))
					{
						input.val($.dateTimeFormat(val,'MM/dd/yyyy h:mmtt'));
					}
					else if (input.is('input.clock'))
					{
						input.val($.dateTimeFormat(val,'h:mmtt'));
					}
					// If this is a textbox, bind the enter function.
					else if (input.is('input:text'))
					{
						// Set the value if we have one, or are editing an existing record.
						if (id || val) input.val(val);

						input.data('grid',grid)
							.data('_id',id)
							.data('oldrows',oldrows||{})
							.data('editrows',editrows)
							.onenter(onsave);
					}
					else if (input.is('select'))
					{
						// Add the list items.
						var f = grid.FieldPropertyList[name];
						grid.addOptions(input,f.List);

						// Set the value if we have one, or are editing an existing record.
						if (id || val) input.val(val);
					}
					else if (input.is('table'))
					{
						// If we have a table, break apart the value and assign each part to the sub inputs.
						input.find('input').each(function(i)
							{
								var el = $(this);
								var len = $.toInt(el.attr('maxlength'));
								if (len)
								{
									var v = (''+val).substr(0,len);
									val = (''+val).substr(len);
									el.val(v||'');
								}
								else
								{
									el.val(val);
									val = '';
								}
							})
							.bind('keydown',grid.moveNext)
							.data('grid',grid)
							.data('_id',id)
							.data('oldrows',oldrows||{})
							.data('editrows',editrows)
							.onenter(onsave);
					}

					else if (id || val)
						input.val(val);

				});

				// Set up the save button.
				editrows.find("a[href^='javascript:void('][href*=Save]")
					.data('_id',id)
					.data('grid',grid)
					.data('oldrows',oldrows||{})
					.data('editrows',editrows)
					.click(onsave);

				// And the cancel button.
				editrows.find("a[href^='javascript:void('][href*=Cancel]")
					.data('grid',grid)
					.data('oldrows',oldrows||{})
					.data('editrows',editrows)
					.click(oncancel);

				// Set up the calendar and clock functions.
				editrows.find('input.calendar').calendar({onfocus:true});
				editrows.find('input.calendarclock').calendar({clock:true,onfocus:true});
				editrows.find('input.clock').calendar({clock:true,onfocus:true,nodate:true});

				// Bind the escape function.
				editrows[0].escapefn = function(e){oncancel.call(grid,e);};
				$(document).escape(editrows[0].escapefn,{grid:grid,oldrows:oldrows,editrows:editrows});

				// If we have validators to deal with, bind them now.
				if (grid.CMSValidation)
					grid.initValidators(editrows);
			},

		// A reference to the current list of validators.
		validatorList : null,

		// Initialize the validators in the edit rows.
		initValidators : function(rows)
			{
				var grid = this;
				grid.validatorList = [];
				
				// Search for all validator controls.
				rows.find('[_validator=true]').each(function(i){
					// Look for the control to validate.
					var validator = $(this);
					var id = validator.attr('controltovalidate_');
					var ctrl;
					if (id && (ctrl=rows.find("[name='"+id+"']")).length)
					{
						// Add a reference to the validator, and bind the blur event.
						validator.data('grid',grid).data('control',ctrl);
						var array = ctrl.data('grid',grid).data('validators')||[];
						array.push(validator);
						ctrl.data('validators',array);
						
						// If we don't have a direct input element, bind to the child input elements.
						if (!ctrl.is('input,textarea,select'))
							ctrl.find('input,textarea,select').data('parent',ctrl).blur(grid.validateControl);
						else
							ctrl.blur(grid.validateControl)
						grid.validatorList.push(validator);
					}

					// Also look for a control to compare, if we need to bind it.
					id = validator.attr('controltocompare_');
					var compare;
					if (id && (compare=rows.find("[name='"+id+"']")).length)
					{
						// Add a reference to the compare control.
						validator.data('compare',compare);
					}
				});
			},

		// Validate a specific control.
		validateControl : function(e)
			{
				var ctrl = $(this);
				ctrl = ctrl.data('parent')||ctrl;
				var grid = ctrl.data('grid');
				var validators = ctrl.data('validators');
				if (validators)
				{
					for (var i=0;i<validators.length;i++)
					{
						var validator = validators[i];
						if (!grid.isValid(validator,ctrl,e))
							validator.show();
						else
							validator.hide();
					}
				}
			},

		// Check each of the validators on the page.
		validateAll : function()
			{
				var grid = this;
				if (!grid.CMSValidation || grid.validatorList==null || grid.validatorList.length==0)
					// If we don't have any validators, then we'll all good.
					return true;
				else
				{
					// Check each of the validators.
					var valid = true;
					for (var i=0;i<grid.validatorList.length;i++)
					{
						var validator = grid.validatorList[i];
						var ctrl = validator.data('control');
						if (!grid.isValid(validator,ctrl))
						{
							valid = false;
							validator.show();
						}
						else
							validator.hide();
					}
					// If any validators are bad, the page cannot be submitted.
					return valid;
				}
			},

		// Check a specific control for validation.
		isValid : function(validator,ctrl,e)
			{
				// First, we'll get the value of the control.
				var val;
				if (ctrl.is('input,textarea,select'))
					val = ctrl.val();
				else
				{
					switch (ctrl.attr('icobalt'))
					{
						case 'System.Web.UI.WebControls.CheckBoxList':
							alert('CheckBoxList validation not operational.');
							return false;
							break;
						case 'System.Web.UI.WebControls.RadioButtonList':
							alert('RadioButtonList validation not operational.');
							return false;
							break;
						case 'Topaz.Controls.CardExpControl':
							val = '';
							ctrl.find('select').each(function(i){
								if (i==0)
									val = $(this).val();
								else
									val = val+'/'+$(this).val();
							});
							break;
						case 'Topaz.Controls.PhoneControl':
							val = '';
							ctrl.find('input:text').each(function(i){
								val += $(this).val();
							});
							break;
					}
				}

				// Next, we'll perform the validation.
				switch (validator.attr('icobalt'))
				{
					case 'System.Web.UI.WebControls.RequiredFieldValidator':
						return val?true:false;
					case 'System.Web.UI.WebControls.RegularExpressionValidator':
						if (!val)
							return true;
						else
						{
							// Get the validation expression.
							var expr = validator.attr('validationexpression_');
							if (!expr)
							{
								expr = validator.attr('validationexpression__');
								if (expr)
									expr = $.decode(expr);
								else
								{
									switch ((validator.attr('expression_')||'').toLowerCase())
									{
										case 'emailaddress':
											expr = "^\\S+@\\S+\\.\\S{2,3}$";
											break;
										case 'websiteurl':
											expr = "^(((ht|f)tp(s?))\\://)?(www.|[a-zA-Z0-9].)[a-zA-Z0-9\\-\\.]+\\.[a-zA-Z]{2,6}(\\:[0-9]+)*(/($|[a-zA-Z0-9\\.\\,\\?\\'\\\\\\+&amp;%\\$#\\=~_\\-]+))*$";
											break;
										case 'strongpassword':
											expr = "^\\*+$|(?=[\\S ]{8})[\\S ]*\\d[\\S ]*";
											break;
										case 'urlsafe':
											expr = "^\\w+$";
											break;
										case 'imagesonly':
											expr = "[\\s\\S]+?\\.(?:jpg|jpe|jpeg|gif|bmp|png)$";
											break;
										default:
											alert('No validation expression for the RegularExpressionValidator.');
											return false;
									}
								}
							}
							
							// Process the expression.
							try
							{
								var regex = new RegExp(expr);
								return regex.test(val);
							}
							catch(ex)
							{
								alert('Illegal validation expression.');
								return false;
							}
						}
					case 'System.Web.UI.WebControls.CompareValidator':
						var control = validator.data('control');
						var compare = validator.data('compare');
						return (control && compare && control.val()==compare.val());
					case 'System.Web.UI.WebControls.RangeValidator':
						alert('RangeValidator not operational.');
						return false;
					case 'Topaz.Controls.CheckBoxValidator':
						if (ctrl.is('input:checkbox'))
							return ctrl[0].checked;
						else
						{
							alert('The CheckBoxValidator can only be run against a checkbox.');
							return false;
						}
					case 'Topaz.Controls.CardExpValidator':
						alert('CardExpValidator not operational.');
						return false;
					case 'Topaz.Controls.CreditCardValidator':
						alert('CreditCardValidator not operational.');
						return false;
					case 'Topaz.Controls.PhoneControlValidator':
						var inputs = ctrl.find('input:text');
						var npa = inputs.eq(0);
						var nxx = inputs.eq(1);
						var suffix = inputs.eq(2);
						var ext = inputs.eq(3);

						// Are we still focused on one of the first two textboxes?
						var focused = e && (npa[0]==e.target||nxx[0]==e.target);

						if (!val)
							// This is not the same as a required field.
							return true;
						else if (focused && /^\d+$/.test(val))
							// If the user is still typing the phone number, just make sure its digits only.
							return true;
						else
							// Otherwise, make sure the phone number was filled out correctly.
							return /^[2-9]\d{2}$/.test(npa.val()) &&
								/^[2-9]\d{2}$/.test(nxx.val()) &&
								/^\d{4}$/.test(suffix.val()) &&
								(!ext.val() || /^\d+$/.test(ext.val()));
					case 'Topaz.Controls.RequiredIfValidator':
						alert('RequiredIfValidator not operational.');
						return false;
					default:
						alert('Unknown validator type.');
						return false;
				}
			},

		onEditItem : function(editrows,oldrows) {},
		onSaveItem : function(newRow,rowData) {},
		onCancelItem : function(oldrows) {},

		// We'll move the focus to the next it automatically if the textbox is full.
		moveNext : function(e)
			{
				// Get the input element and its max length.
				var input = $(this);
				var len = $.toInt(input.attr('maxlength'));
				
				// If a max length is defined.
				if (len)
				{
					// And we've reached it.
					if (input.val().length==len-1)
					{
						// If we've pressed a regular key
						if (e.which>=32)
						{
							// Look for the next input element.
							var found = false;
							var next = null;
							input.parents('table:first').find('input').each(function(i){
								if (found)
								{
									if (!next) next = $(this);
								}
								else if (this==input[0])
									found = true;
							});
							
							// If we have a next element.
							if (next)
							{
								// Focus input on a timeout.
								var fn = function(next){
									return function(){
										next.focus();
										next = null;
									};
								}(next);
								setTimeout(fn,1);
							}
						}
					}
					else if (input.val().length<len-1 && e.which==9 && !e.shiftKey)
						// If we haven't filled the textbox but we tried to tab forward, cancel.
						return false;
				}
			},

		// Cancel the editing.
		cancelEdit : function(e)
			{
				var grid,oldrows,editrows;
				if (e && e.data && e.data.grid)
				{
					grid = e.data.grid;
					oldrows = e.data.oldrows;
					editrows = e.data.editrows;
				}
				else
				{
					var el = $(this);
					grid = el.data('grid');
					oldrows = el.data('oldrows');
					editrows = el.data('editrows');
				}

				// unbind the escape function.
				var fn = editrows[0].escapefn;
				$(document).unescape(fn);
				editrows[0].escapefn = null;

				// Any cleanup needed before removing the edit rows.
				grid.cleanupEdit(editrows);
				
				// Remove the edit row and show the original.
				editrows.remove();
				oldrows.show();

				// Fire any cancel event.
				grid.onCancelItem(oldrows);
			},

		// Placeholder method.
		cleanupEdit : function(editrows){},

		// Get default values (primarily for a new record being added to the system.
		getDefaults : function(e)
			{
				var data = {};
				for (var f in this.FieldPropertyList)
				{
					var p = this.FieldPropertyList[f];
					var d = p.Default;
					if (d)
					{
						// Grab each of the values and add them if we actually have one.
						var val = d.DefaultValue || this.getProperty(d.DefaultValueField,d.DefaultValueType,null);
						if (val) data[p.Name||f] = val;
					}
				}
				return data;
			},

		// Save a change to an edited row.
		saveEdit : function(e)
			{
				var el = $(this);
				var id = el.data('_id');
				var grid = el.data('grid');
				var oldrows = el.data('oldrows');
				var editrows = el.data('editrows');

				// Validate the form before going any further.
				if (!grid.validateAll())
					return false;

				// Assign the values.
				var row = {};
				var fields = [];
				var values = [];
				editrows.find('[name]').each(function(i){
					var input = $(this);
					var name = input.attr('name');
					if (input.is('input:checkbox'))
						val = ''+input[0].checked;
					else if (input.is('table'))
					{
						// If this is a table with child inputs, add each of their values.
						val = '';
						input.find('input').each(function(i){
							val += $(this).val();
						});
					}
					else
						val = input.val();

					fields.push(name);
					values.push($.encode(val));
					row[name] = val;
				});

				// If we're inserting a new draggable row at the bottom, adjust the Seq value to drop it at the end.
				if (!id && grid.EditData.DragCell>0 && !grid.EditData.NewRowOnTop)
				{
					var field = grid.FieldNames[grid.EditData.DragCell-1];
					var seq = fields.indexOf(field);
					var lastseq = 32767+(grid.seqbase||0);
					if (seq<0)
					{
						fields.push(field);
						values.push(lastseq);
					}
					else
						values[seq] = lastseq;
				}

				// Validate the row.
				var valid = grid.validateSave(row);
				if (valid)
				{
					alert(valid);
					return false;
				}

				// Get the url.
				var url = $.getAjaxUrl(window.location.href,{AjaxEdit:grid.element.attr('id'),I:id,F:fields.join(','),V:$.encode(values.join(','))});
				if (url)
				{
					var el = this;
					var evt = $.extend({},e);
					grid.element.loading(grid.timeout,{mrelative:editrows.eq(0)});
					$.ajax(
						{
							url:url,
							dataType:'json',
							success:function(results)
								{
									// Handle the loading icon.
									grid.element.doneLoading();

									// Notify the user of any errors.
									if (results && results.Error)
									{
										// Check to see if we have a control on the page to render the error message.
										var control = false;
										if (results.Error.Control)
										{
											for (var i=0;i<results.Error.Control.length;i++)
											{
												// If we find a control.
												var ctrl = editrows.find("[id='"+results.Error.Control[i]+"']");
												if (ctrl.length)
												{
													// Set the message as appropriate and make it visible.
													if (ctrl.is(':visible') || $.trim(ctrl.html()||'').length==0)
														ctrl.html(results.Error.Message);
													ctrl.show();
													control = true;
												}
											}
										}

										// If we didn't find a control, alert the error message.
										if (!control && results.Error.Message)
											alert(results.Error.Message.replace(/<br>/gi,''));

										// Focus on an input element and null out the closure.
										var input = editrows.find('input:text:first');
										setTimeout(function(){input.focus();},1);
										grid = null;
										row = null;
										evt = null;
										el = null;
										return;
									}

									// Rebuild the row with the new key.
									var rowData = results.Rows && results.Rows.length && results.Rows[0];
									var html = grid.parseTemplate(grid.ItemTemplate,rowData);
									var newRow = $(html).insertAfter(oldrows.eq(oldrows.length-1));
									
									// Move the new row into position.
									if (id || grid.EditData.NewRowOnTop)
										newRow.insertBefore(editrows.eq(0));
									else
										newRow.insertBefore(grid.insertPoint);

									// If we had an id already, its an update.
									if (id)
									{
										// Fire the update event, if there is one.
										if ($.isFunction(grid.OnAjaxUpdate))
											grid.OnAjaxUpdate(oldrows,newRow,rowData,editrows);

										// If we had an id, then this isn't an add new, so remove the old row, and update the results data.
										oldrows.remove();
										grid.updateResultsRow(id,rowData);
									}
									else
									{
										// Fire the insert event, if there is one.
										if ($.isFunction(grid.OnAjaxInsert))
											grid.OnAjaxInsert(newRow,rowData,editrows);

										// Add the new results data to the stack.
										if (!grid.Results) grid.Results = [];
										grid.Results.push(rowData);
									}

									// Any cleanup needed before removing the edit rows.
									grid.cleanupEdit(editrows);

									// Remove the edit row.
									editrows.remove();

									grid.initDrag(newRow);
									grid.initEdit(newRow);
									grid.initDelete(newRow);

									// Re-set up the rows.
									grid.resetRows();

									// Fire this event after the save is completed.
									grid.onSaveItem(newRow,rowData);

									// Clear out the closure variables.
									grid = null;
									row = null;
									evt = null;
									el = null;
								}
						});
				}

				return false;
			},

		// Validate the saved data.
		validateSave : function(row)
			{
				return null;
			},

		// Delete the row in question.
		deleteItem : function(e)
			{
				var el = $(this);
				var id = el.data('_id');
				var grid = el.data('grid');
				var rows = el.data('rows');

				if (confirm("Are you sure you want to delete this item?"))
				{
					var url = $.getAjaxUrl(window.location.href,{AjaxDelete:grid.element.attr('id'),I:id});
					if (url)
					{
						grid.element.loading(grid.timeout,{mrelative:rows.eq(0)});
						$.ajax({url:url,dataType:'json',success:function(result)
							{
								grid.element.doneLoading();

								if (result.Error)
									alert(result.Error);
								else if (result.Success)
								{
									// Remove the row.
									rows.remove();

									// Handle any delete event.
									if ($.isFunction(grid.OnAjaxDelete))
										grid.OnAjaxDelete(grid);

									// Re-initialize the remaining rows.
									grid.resetRows();
									grid.initDrag();
								}

								// Null the closure variable.
								rows=null;
							}});
					}
				}
				return false;
			},

		// Update the grid with the latest search criteria.
		updateSearch : function(pageid,sort,dir,results)
			{
				// Update the pageid, if supplied.
				if (pageid=='Next')
					this.PageID++;
				else if (pageid)
					this.PageID = $.toInt(pageid);

				// Update the sort order, if supplied.
				if (sort)
				{
					switch (dir)
					{
						case "2":
							this.SortOrder = ' DESC';
							break;
						default:
							this.SortOrder = ' ASC';
							break;
					}
					this.SortBy = sort;
					
					if (this.Results)
					{
						// Re-sort the result set with an anonymous delegate.
						var grid = this;
						var sort = this.SortBy;
						var up = this.SortOrder==' DESC';
						this.Results.quickSort(function(a,b) { return grid.sortCompare(a,b,sort,up); });
						this.filterResults();
						grid = null;
					}
				}
				
				// Update the results per page, if supplied.
				if (results)
				{
					this.ResultsPerPage = $.toInt(results);
					if (this.ResultsPerPage<=0)
						this.ResultsPerPage = 20;
				}
				
				this.renderResults();
			},

		// Function to handle the compareison.
		sortCompare : function(a,b,sort,up)
			{
				// Get the specifi fields to compare.
				var val1 = a[sort];
				var val2 = b[sort];

				// First handle nonexistant values.
				var null1 = typeof(val1)=='undefined' || val1==null;
				var null2 = typeof(val2)=='undefined' || val2==null;
				if (null1 && null2)
					return 0;
				else if (null1 && !null2)
					return up ? 1 : -1;
				else if (!null1 && null2)
					return up ? -1 : 1;
				else if (val1.constructor==Date)
					return up ? val2.getTime()-val1.getTime() : val1.getTime()-val2.getTime();
				else
				{
					if (val1==val2)
						return 0;
					else if (val1 < val2)
						return up ? 1 : -1;
					else
						return up ? -1 : 1;
				}
			},

		// Filter the results based on the current search criteria.
		filterResults : function()
			{
				// If we don't have any results, note the pending action flag.
				if (!this.Results)
				{
					this.pendingAction = true;
					this.element.loading();
					return false;
				}

				// Then we filter the rows.
				var results = [];
				if (!this.SearchProperties)
					results = this.Results;
				else
					for (var i=0;i<this.Results.length;i++)
					{
						// Add any valid rows.
						var row = this.Results[i];
						if (this.validRow(row))
							results.push(row);
					}

				// If our starting point is past the length of the grid, move back to the beginning.
				var start = this.Paging ? (this.PageID-1) * this.ResultsPerPage : 0;
				if (start > results.length)
				{
					this.PageID = 1;
				}

				// Record the new total results.
				this.Filtered = results;
				this.TotalResults = results.length;
			},

		// Update any dynamically built search lists.
		updateSearchLists : function()
			{
				// Iterate through each of the search properties.
				for (var p in this.SearchProperties)
				{
					// If this element has a filtered datasource 
					var s = this.SearchProperties[p];
					this.addOptions(s.Element,s);
				}
			},

		// Add options to a select element, based on a list datasource.
		addOptions : function(element,s)
			{
				if (element.is('select') && s.DataSource && s.DataSource.constructor==Array)
				{
					// Build a list of filters.
					var filters = [];
					
					if (s.FilterBy)
					{
						for (var i=0;i<s.FilterBy.length;i++)
						{
							// Get the field
							var field = s.FilterByValueField && s.FilterByValueField.length>i && s.FilterByValueField[i];
							if (field)
							{
								// And the type and match
								var type = s.FilterByValueType && s.FilterByValueType.length>i && s.FilterByValueType[i];
								var match = s.FilterByMatch && s.FilterByMatch.length>i && s.FilterByMatch[i];
								
								// And the value.
								var val = null;
								switch (type)
								{
									case "Control":
										val = this.SearchProperties[field].Element.val();
										break;
									case "QueryString":
										val = this.getQueryString(field);
										break;
									case "Default":
										val = field;
										break;
								}
								
								if (val!=null) filters.push({FieldName:s.FilterBy[i],Match:s.Match,Value:val});
							}
						}
					}

					// Clear out the existing options.
					var options = element[0].options;
					options.length = 0;

					// Add the optional first item.
					var count = 0;
					if (s.AddFirstItem)
						options[count++] = new Option(s.AddFirstItem=="&nbsp;"?' ':s.AddFirstItem,'');

					// Add any maching rows.
					this.verifyListFields(s);
					for (var i=0;i<s.DataSource.length;i++)
					{
						var row = s.DataSource[i];

						// Ensure we have a valid row based on any supplied filters.
						var valid = true;
						for (var j=0;j<filters.length;j++)
						{
							// Check to see if it is valid.
							if (!this.validValue(row,filters[j],filters[j].Value,true))
							{
								valid = false;
								break;
							}
						}

						// If we have a valid row, add it.
						if (valid)
							options[count++] = new Option(row[s.TextField],row[s.ValueField]);
					}
				}
			},

		// Based on the current search criteria, render the results.
		renderResults : function()
			{
				// If we don't have any results, note the pending action flag.
				if (!this.Results)
				{
					this.pendingAction = true;
					this.element.loading();
					return;
				}
				else
				{
					this.element.doneLoading();
					this.pendingAction = false;

				}
				if (!this.Filtered) this.filterResults();
				var results = this.Filtered;

				// Get the start and end.
				var start = this.Paging ? (this.PageID-1) * this.ResultsPerPage : 0;
				var end = this.Paging ? this.PageID * this.ResultsPerPage : results.length;
				if (end > results.length) end = this.Results.length;

				// If our starting point is past the length of the grid, move back to the beginning.
				if (start > results.length)
				{
					start = 0;
					end = this.ResultsPerPage;
					this.PageID = 1;
				}
				
				// If our ending point is past the length of the grid, move it to the end.
				if (end > results.length) end = results.length;

				// Remove the old rows.
				var filter = this.RowsFilter;
				if (filter.indexOf('[icobalt')==0)
					this.element.find(filter).remove();
				else
					this.element.children('tbody').children(filter).remove();

				// Iterate through the rows to display.
				var count = 0;
				var grid = this;
				for (var i=start;i<end;i++)
				{
					// Get the row and add it.
					var row = results[i];
					var html = ++count%2 ? this.ItemTemplate : (this.AlternatingItemTemplate || this.ItemTemplate);
					this.insertPoint.before(grid.parseTemplate(html,row));
				}
				grid = null;
				results = null;

				// If we're in edit mode, re-initialize the edit links.
				if (this.EditData)
				{
					// Get the data rows.
					var rows = this.getDataRows();

					// Initialize any editing modes.
					if (this.EditData.EditCell > 0)
						this.initEdit(rows);

					// If have a drag sell, set that up as well.
					if (this.EditData.DragCell>0)
						this.initDrag(rows);

					// Initialize any delete buttons.
					if (this.EditData.DeleteCell > 0)
						this.initDelete(rows);
				}

				// Handle paging if necessary.
				if (this.Paging)
					this.renderPaging();

				// If a client-side onrender event was defined, fire it.
				if ($.isFunction(this.OnAjaxRender))
					this.OnAjaxRender(this);
			},

		// Do we have a valid row to render?
		validRow : function(row)
			{
				// If we have no search criteria, its automatically good.
				if (!this.SearchProperties)
					return true;

				// Iterate through the search properties.
				for (var p in this.SearchProperties)
				{
					// Get the current value.
					var search = this.SearchProperties[p];
					var val = search.Element.val();
					
					// Checkbox search elements are handled differently.
					if (search.Element.is('input:checkbox'))
					{
						val = search.Element[0].checked;
						if (!val)
							return true;
					}

					// Check to see if it is valid.
					if (!this.validValue(row,search,val))
						return false;
				}

				// If we've made it this far, we're good.
				return true;
			},

		// Verify a specific value.
		validValue : function(row,search,val,strict)
			{
				// If the search field doesn't exist in the results set (such as when used by a stored procedure)
				// Do not use this as a filter criteria.
				var f = search.FieldName;
				var pos = f.indexOf('__');
				var rowVal = pos>0 ? row[f.substr(0,pos)] : row[f];

				if (typeof(rowVal)=='undefined')
					return true;

				// Ensure we aren't dealing with a null value.
				if (val==null) val = '';
				if (rowVal==null) rowVal = '';

				// If we have a valid value (or we are doing a strict comparison).
				if (val || strict)
				{
					if (typeof(rowVal)=='number')
						val = $.toFloat(val);
					else if (typeof(rowVal)=='boolean')
						val = $.toBool(val);
					else if (rowVal.constructor==Date)
					{
						val = $.cobalt.calendar.parseDate(val,true);
						if (val==null) return true;
					}
				
					// Check the match.
					if (!search.Match)
						return (rowVal == val);
					else
					{
						switch (search.Match)
						{
							case "LIKE":
								if (rowVal.toLowerCase().indexOf(val.toLowerCase()) < 0) return false;
								break;
							case "LEFT":
								if (rowVal.toLowerCase().indexOf(val.toLowerCase()) != 0) return false;
								break;
							case "GREATER":
								if (rowVal <= val) return false;
								break;
							case "LESSER":
								if (rowVal >= val) return false;
								break;
							case "GREATEROREQUAL":
								if (rowVal < val) return false;
								break;
							case "LESSEROREQUAL":
								if (rowVal > val) return false;
								break;
							case "NOTEQUAL":
								if (rowVal == val) return false;
								break;
							case "RIGHT":
								if (rowVal.toLowerCase().indexOf(val.toLowerCase()) != rowVal.length-val.length) return false;
								break;
							case "NOTLIKE":
								if (rowVal.toLowerCase().indexOf(val.toLowerCase()) >= 0) return false;
								break;
							case "NOTLEFT":
								if (rowVal.toLowerCase().indexOf(val.toLowerCase()) == 0) return false;
								break;
							case "NOTRIGHT":
								if (rowVal.toLowerCase().indexOf(val.toLowerCase()) == rowVal.length-val.length) return false;
								break;
							default:
								if (rowVal != val) return false;
								break;
						}
					}
				}
				
				return true;
			},

		// Render the paging notes.
		renderPaging : function()
			{
				var totalPages = Math.ceil(this.TotalResults / this.ResultsPerPage);
				var pageid = this.TotalResults==0 ? 0 : this.PageID;
				this.pagingSummary.html('<span>'+this.TotalResults+' results found.&nbsp;&nbsp;Viewing page <b>'+pageid+'</b> of <b>'+totalPages+'</b></span>');
				this.navigation.html(this.renderNavigation(this.PageID,totalPages));
				this.navigation.children('a').bind('click',{grid:this},this.handleNav);
			},

		// Render the navigation panel.
		renderNavigation : function(PageID,TotalPages)
			{
				var firstsection;
				var middlesection;
				var lastsection;
				var placeholder = [];

				// We will not return any navigation controls if there is no paging.
				if (PageID == 0 || TotalPages <= 1)
				{
					return "&nbsp;";
				}

				// The navigation can be continuous, or broken into two or three parts.
				if (TotalPages <= 17)
				{
					// Up to 17 total pages will be displayed together.
					firstsection = TotalPages;
					middlesection = 0;
					lastsection = 0;
				}
				else if (PageID < 9 || PageID + 6 > TotalPages)
				{
					// More than 17 pages will be split into two parts.
					firstsection = 9;
					middlesection = 0;
					lastsection = 7;
				}
				else
				{
					// Unless the current page is in the middle, then three parts.
					firstsection = 5;
					middlesection = PageID - 3;
					lastsection = 3;
				}

				placeholder.push("Goto page ");

				// Add the links in the first section.
				for (var i = 1; i <= firstsection; i++)
				{
					placeholder.push(this.writeNavLink(i));
					placeholder.push("&nbsp;");
				}

				// Add the links in the second section.
				if (middlesection > 0)
				{
					placeholder.push("&nbsp;<b>.&nbsp;.&nbsp;.</b>&nbsp");
					for (var i = middlesection; i < middlesection + 7; i++)
					{
						placeholder.push(this.writeNavLink(i));
						placeholder.push("&nbsp;");
					}
				}

				// Add the link to the last section.
				if (lastsection > 0)
				{
					placeholder.push("&nbsp;<b>.&nbsp;.&nbsp;.</b>&nbsp;");
					for (var i = (TotalPages - lastsection) + 1; i <= TotalPages; i++)
					{
						placeholder.push(this.writeNavLink(i));
						placeholder.push("&nbsp;");
					}
				}

				// Add a "Next" link if we are not at the end.
				if (PageID < TotalPages)
				{
					placeholder.push("&nbsp;&nbsp;");
					placeholder.push(this.writeNavLink(PageID + 1, "Next"));
				}

				return placeholder.join('');
			},

		// Write a single navigation link.
		writeNavLink : function(pageid,text)
			{
				text = text || (pageid==this.PageID ? '<b>'+pageid+'</b>' : pageid);
				return '<a id="'+this.id+'_ctl00_Nav_'+pageid+'" href="javascript:void(0)">'+text+'</a>';
			},

		// Format based on defined functions.
		format : function(f,row,pattern)
			{
				var pos = f.indexOf('__');
				var val = (typeof(row)=='undefined' || row==null) ? null : (pos>0 ? row[f.substr(0,pos)] : row[f]);
				var field = this.FieldPropertyList[f];

				// If we have no field properties defined, return the value straight.
				if (!field) return val;

				// If we have a list, look up the value.
				if (field.List && field.List.DataSource)
					val = this.processList(field.List,row,val);

				// Handle any special formatting.
				var fn;
				if (val!=null && field && field.Format && (fn=$.cobalt.ajaxgrid.formatting[field.Format.Type])!=null)
					val = fn(val,field.Format,row);

				// If we have an image to render, do so now.
				if (field.Image)
					return this.processImage(field.Image,row,val);

				// If we have an link to render, do so now.
				if (field.Link)
					return this.processLink(field.Link,row,val);

				return val==null?'':val;
			},

		// Process a list.
		processList : function(list,row,v)
			{
				// If we don't have any items, do nothing.
				if (list.DataSource.length==0)
					return '';

				// Iterate through the items, looking for a match.
				this.verifyListFields(list);
				for (var i=0;i<list.DataSource.length;i++)
				{
					var item = list.DataSource[i];
					
					// If we find a match, return its text value.
					if (item && item[list.ValueField] == v)
						return item[list.TextField];
				}
				
				// No match.
				return '';
			},

		// Verify that we have properly defined list fields.
		verifyListFields : function(list)
			{
				// Ensure we have something to verify.
				if (!list || !list.DataSource || !list.DataSource.length) return;

				// Ensure we have a default TextField and ValueField.
				if (!list.ValueField || !list.TextField)
				{
					var count = 0;
					for (var p in list.DataSource[0])
					{
						if (count==0 && !list.ValueField)
							list.ValueField = p;
						else if (count==1 && !list.TextField)
							list.TextField = p;
						
						if (++count>1)
							break;
					}
				}
			},

		// Process the value as an image.
		processImage : function(image,row,val)
			{
				var img  = (image.ImageUrl||'').replace('~/','');
				
				if (image.ImageValueField)
					img  = img  && this.dynamicProperties(img , null, link.ImageValueField, link.ImageValueType, row);
				else if (this.CMSTokens)
				{
					img = img
						.replace(this.r_cmstokens,function(s,f1,f2)
							{
								var f = f2||f1;
								var val = row && row[f];
								if (typeof(val)!='undefined')
								{
									// If we don't have any format types, return the value directly.
									if (!f2)
										return (val!=null && val.constructor==Date) ? $.dateTimeFormat(val) : val;

									// Check for an handle any formatting.
									switch (f1.toLowerCase())
									{
										case "http":
											return val.indexOf('http')!=0 ? 'http://'+val : val;
										case "www":
											if (val && val.indexOf('www')!=0 && val.split('.').length==2)
												return 'www.'+val;
											else
												return val;
										case "nohttp":
											return val.replace(/^https?:\/\//i,'');
										case "encode":
											return $.encode(val);
										case "decode":
											return $.decode(val);
										case "checked":
											return $.toBool(val) ? 'checked' : '';
										case "initial":
											if (val)
											{
												val = $.trim(val);
												if (val.length && val.indexOf('.')!=val.length-1)
													return val+'.';
												else
													return val;
											}
											else
												return val;
										case "random":
											var range = $.toInt(val);
											return range ? ''+parseInt(Math.random()*range) : '';
										default:
											return s;
									}
								}
								else
									return s;
							});
				}
				else
					img = img?img.replace('{0}',val):val;

				// Assign any thumbnails.
				if (image.Thumbnail)
				{
					var root = $(document.body).attr('_root');
					img =
						'images/thumbnail.aspx'+
						"?I=" + $.encode(root+img) +
						"&W=" + $.toInt(image.Width) +
						"&H=" + $.toInt(image.Height);
				}

				// Return the image.
				return '<img src="'+img+'" border="'+$.toInt(image.Border)+'">';
			},

		// Process the value as a link.
		processLink : function(link,row,val)
			{
				var text = link.LinkText || val;
				if (text!=null) text = text.toString();
				var href = (link.NavigateUrl||'').replace('~/','');
				var img  = (link.ImageUrl||'').replace('~/','');
				
				if (link.LinkValueField)
				{
					text = text && this.dynamicProperties(text, null, link.LinkValueField, link.LinkValueType, row);
					href = this.dynamicProperties(href||'{0}', null, link.LinkValueField, link.LinkValueType, row);
					img  = img  && this.dynamicProperties(img , null, link.LinkValueField, link.LinkValueType, row);
				}
				else
					href = href.replace('{0}',val);

				// If we have an image, replace the standard link text.
				if (img) text = '<img src="'+img+'" border=0>';
				var target = (link.Target && ' target="'+link.Target+'"') || '';
				
				return '<a href="'+href+'"'+target+'>'+text+'</a>';
			},

		// Process dynamic properties.
		dynamicProperties : function(data,values,fields,types,row)
			{
				// If there are no keys defined, return the string unchanged.
				var valuelength = values ? values.length : 0;
				var fieldlength = fields ? fields.length : 0;
				var typelength = types ? types.length : 0;
				if (valuelength + fieldlength + typelength == 0) return data;

				// Get the size of the array.				
				var length = valuelength;
				if (fieldlength > length) length = fieldlength;
				if (typelength > length) length = typelength;

				// Iterate through the fields.
				for (var i = 0; i < length; i++)
				{
					// Pull the key and the source, defaulting to obtaining field data.
					var val = (values && values.length > i) ? values[i] : "";
					var field = (fields && fields.length > i) ? fields[i] : "";
					var type = (types && types.length > i) ? types[i]||"Field" : "Field";

					// Get the actual dynamic value.
					var element = val || this.getProperty(field, type, row) || '';

					// Replace the token with the actual value.
					data = data.replace("{"+i+"}",element);
				}
				return data;
			},

		// Get a specific property.
		getProperty : function(field, type, row)
			{
				switch (type)
				{
					case "Field":
						return row && row[field];
					case "QueryString":
						return this.getQueryString(field);
					case "GetDate":
						return new Date();
					default:
						return '';
				}
			},

		// Get a querystring value.
		getQueryString : function(field)
			{
				// If the querystring hasn't been built out already, do so now.
				if (!this.query)
				{
					// Grab the querystring part of the url.
					this.query = {};
					var items = window.location.href.split('?').pop().split('&');
					
					// Iterate through each of the elements.
					for (var i=0;i<items.length;i++)
					{
						// Assign anything that has a proper name/value pair.
						var pair = items[i].split('=');
						if (pair.length==2)
							this.query[pair[0].toLowerCase()] = pair[1];
					}
				}
				
				// Return the querystring.
				if (field) field = field.toLowerCase();
				return this.query[field]||'';
			}
	});

	// Run the quick sort algorithm on an array.
	$.cobalt.quickSort = function(array, begin, end, fn)
	{
		if(end-1>begin) {
			var pivot=begin+Math.floor(Math.random()*(end-begin));

			pivot=$.cobalt.quickPartition(array, begin, end, pivot, fn);

			$.cobalt.quickSort(array, begin, pivot, fn);
			$.cobalt.quickSort(array, pivot+1, end, fn);
		}
	};

	// The partition method used by the quicksort algorithm.
	$.cobalt.quickPartition = function(array, begin, end, pivot, fn)
	{
		var piv=array[pivot];
		array.swap(pivot, end-1);
		var store=begin;
		var ix;
		for(ix=begin; ix<end-1; ++ix) {
			if(fn ? fn(array[ix],piv)<=0 : array[ix]<=piv) {
				array.swap(store, ix);
				++store;
			}
		}
		array.swap(end-1, store);

		return store;
	};

	// Add an array swap method.
	Array.prototype.swap=function(a, b)
	{
		var tmp=this[a];
		this[a]=this[b];
		this[b]=tmp;
	}

	// Add a quickSort prototype method.
	Array.prototype.quickSort = function(fn)
	{
		$.cobalt.quickSort(this,0,this.length,fn);
	};

})(jQuery);
