

//language object stores all language which is generated through javascript
//NOTE: all values must be simple text, no markup or entities, but unicode is allowed; 
//apostrophes and quote marks must be escaped, or preferably implemented as unicode, eg "\u0027"
var lang = 
{
	
	
	//text for the general notice bar at the top of the page 
	//and the successful creation notice on auction pages
	'noticebar' :
	{
		'label' 		: 'Close', 							//close-icon label
		'tooltip' 		: 'Close this notice'				//close-icon tooltip
	},
		
		
	//text for converting sitepoint datetime microformats
	'datetime' : 
	{
		'units' : 											//date units
		{											
			'year' 		: 'year', 							//year
			'month' 	: 'month', 							//month
			'day' 		: 'day',	 						//day
			'hour' 		: 'hour', 							//hour
			'minute' 	: 'minute', 						//minute
			'second' 	: 'second' 							//second
		},
		'shortunits' : 										//abbreviated date units
		{										
			'year' 		: 'yr', 							//year
			'month' 	: 'mth', 							//month
			'day' 		: 'day',	 						//day
			'hour' 		: 'hr', 							//hour
			'minute' 	: 'min', 							//minute
			'second' 	: 'sec' 							//second
		},
		'today' 		: 'Today at %time',					//short-format sentence for "today at [time]"
		'yesterday' 	: 'Yesterday at %time',				//short-format sentence for "yesterday at [time]"
		'tomorrow' 		: 'Tomorrow at %time',				//short-format sentence for "tomorrow at [time]"
		'imminent'		: 'Less than 1 min',				//an imminent time interval
		'ended'			: 'Ended',							//word for "ended" (to replace time in listings tables)
		'long-ended'	: 'Auction has ended',				//longhand sentence for "ended" (for bid pages)
		'no-time'		: 'None (auction has ended)',		//longhand sentence for "no time" (for auction pages)
		'at'			: 'at',								//word for "at", eg. "Today at 15.45"
		'and'			: 'and',							//word for "and", eg. "3 days and 2 hours"
		'ago'			: 'ago'								//word for "ago", eg. "2 days ago"
	},
	
	
	//text for the stats boxes
	'statsboxes' : 
	{
		'icon-alt'		: '[view stats]',					//icon alt text
		'icon-label'	: 'View Stats'						//icon label text
	},
	
	
	//text for the table tabs on the homepage
	'table-tabs' :
	{
		'this-table'	: 'this table',						//"this table" meta text for current tab
		'loading'		: 'Loading...'						//"loading" text for newly selected tab 
	}, 
	
	
	//text for the show password checkbox 
	'show-password' :
	{
		'label'			: 'Show password'					//label text
	},
	
	
	//text for the select/unselect all checkbox
	'select-all' : 
	{
		'label'			: 'Select/Unselect all'				//label text
	}
	

};


// ******************************************
// CONFIG
// ******************************************



//config object stores all values which you might want to change
var config = 
{


	//***DEV enable dev mode ("yes" or "no")
	'devmode'			: 'no',						
	
	
	
	//options for the SiteManager class
	'manager' :
	{
		'contexts'		: ['listings-table', 'preview-table', 				//array of IDs of container elements with datetime microformats inside them
							'bid-overview', 				//only elements that are specified here will have their microformat elements converted
							'bid-history-table', 			//ie. there is no longer a generic process that scans the whole document looking for them
							'watchlist-table', 				//but you can now specify multiple containers for a page, so no worries there :)
							'inbox-table', 'recent-bids-table', 'recent-listings-table', 'recent-offers-table',
							'message-details', 'user-listings-table', 'auction-history',
							'auction-questions', 'auction-add-question', 'comment-history', 'transactions-table',
                                                        'pending-bids-table', 'pending-offers-table',
							'feedback-invites','unpaidfees', 'feedbackTable'],	
							
		'addtitles'		: 'yes'								//whether to add title attributes to converted datetime spans
                                                            //that contain the original timestamp, converted to a local timestring
                                                            //eg. "11 days [Thursday April 16th, 13:55:08 GMT+0100 2009]" for text that reads "11 days"
	},
	
	
	
	//options for the Homepage class
	'homepage' : 
	{
		'includes' 		: 'includes/frontpage/%file.php' 	//path from the homepage to the listings include files, with filename replacement token
	},
	
	
	
	//options for Beatbox
	'beatbox' :
	{
		'maxsize'		: '0.5',							//max image size (as a proportion of the available window space)
		'thumbnails'	: '.small'							//thumbnail extension value
	},
	
	
	
	//options for StatsBoxes
	'statsboxes' : 
	{
		'opentime'		: '200'								//open time delay to prevent "drive-bys"
                                                            //the final delay will be 100ms + this value
	}
	

};

// HLP1.1 :: General Helper 
//***************************************************************************
// DOM scripting by brothercake -- http://www.brothercake.com/
// GNU Lesser General Public License -- http://www.gnu.org/licenses/lgpl.html
//***************************************************************************





//general helper class
function Helper()
{
	//shortcut reference to body, cos document.body isn't completely reliable
	this.body = document.getElementsByTagName('body').item(0);
}



//shrink an element's height to zero
Helper.prototype.shrink = function(element, resolution, callback)
{
	//get the initial height and stop if it's already less than zero
	var height = element.offsetHeight;
	if(height <= 0) 
	{ 
		return false; 
	}
	
	//create a counter and the amount of reduction per iteration
	var count = 0, step = height / resolution;
	
	//set the first height reduction
	element.style.height = Math.floor(height - (step * (++count))) + 'px';

	//create a timer
	var timer = setInterval(function()
	{
		//define the new height
		var newheight = Math.floor(height - (step * (++count)));
		
		//if it's less than or zero, clear the timer and call the callback
		if(newheight <= 0)
		{
			clearInterval(timer);
			if(typeof callback != 'undefined')
			{
				callback();
			}
			return true;
		}
		
		//otherwise set the new height
		element.style.height = newheight + 'px';
		
	}, 55);
	
	return true;
};



//fade an element's opacity to zero
Helper.prototype.fade = function(element, resolution, callback)
{
	//create a counter and the amount of reduction per iteration
	var count = 0, step = 1 / resolution;
	
	//create an inner function for applying opacity
	//using the standard method and IE's method
	var self = this;
	function setOpacity(value)
	{
		if(self.manager.msie)
		{
			try { element.filters.alpha.opacity = value * 100; }
			catch(err) {}
		}
		else { element.style.opacity = value; }
	}
	
	//apply the first reduction
	setOpacity(1 - (step / 1 * (++count)));
	
	//create a counter
	var timer = setInterval(function()
	{
		//define the new value
		var newopacity = 1 - (step / 1 * (++count));
		
		//if it's less than or zero, clear the timer and call the callback 
		if(newopacity <= 0)
		{
			clearInterval(timer);
			
			if(typeof callback != 'undefined')
			{
				callback();
			}
		}

		//otherwise set the new value		
		setOpacity(newopacity);

	}, 55);
	
	return true;
};




//enable state-scope image replacement
//http://www.sitepoint.com/article/image-replacement-state-scope/
//I've modified the script to use an images-off state rather than images-on
//so that it fails cleanly when javascript is not available
Helper.prototype.enableStateScopeImageReplacement = function()
{
	document.enableStateScope = function(scope, on)
	{
	  var de = document.documentElement;
	  if (on)
		de.className += " " + scope;
	  else
		de.className = de.className.replace(
		  new RegExp("\\b" + scope + "\\b"), "");
	};
	
	var de = document.documentElement;
	var img = new Image();
	
	document.enableStateScope("images-off", true);

	// Handling for Gecko browsers
	if (img.style.MozBinding != null)
	{
		img.style.backgroundImage = "url(" + document.location.protocol + "//0)";
		var bg = window.getComputedStyle(img, '').backgroundImage;
		
		// When images are off, Firefox 2 and lower reports "none"
		// Firefox 3 and higher reports "url(invalid-url:)"
		// Also, always show images for local files in Firefox
		if (bg != "none" && bg != "url(invalid-url:)" || document.URL.substr(0, 2) == "fi")
		{
			document.enableStateScope("images-off", false);
			document.enableStateScope("images-on", true);
		}
	}
	else
	{
		// Handling for Safari (including iPhone)
		img.style.cssText = "-webkit-opacity:0";
		if (img.style.webkitOpacity == 0)
		{
			img.onload = function()
			{
				// Only enable the state scope if the width
				// of the image is greater than 0.
				if(img.width > 0)
				{
					document.enableStateScope("images-off", false);
					document.enableStateScope("images-on", true);
				}
			}
			// Source the image to a 43-byte 1x1 pixel GIF image encoded as a data URI.
			img.src = 
				"data:image/gif;base64," +
				"R0lGODlhAQABAIAAAP///wAAACH5BAE" +
				"AAAAALAAAAAABAAEAAAICRAEAOw==";
		}
		// Handling for everything else
		else
		{
			img.onerror = function(e)
			{
				document.enableStateScope("images-off", false);
				document.enableStateScope("images-on", true);
			}
			img.src = "about:blank";
		}
	}

};



//it's like a dollar function
Helper.prototype.get = function(find, context, conditions)
{
	//get a single element
	if(find.indexOf('#') != -1)
	{
		return document.getElementById(find.split('#')[1]);
	}

	//get a collectoin of elements
	else 
	{
		//use document as the context unless another is specified
		if(typeof context == 'undefined') 
		{ 
			context = document; 
		}
		
		//get the collection and convert to array
		var nodes = this.nodeListToArray(context.getElementsByTagName(find));
		
		//if we have no conditions, return the array and we're done
		if(this.empty(conditions))
		{
			return nodes;
		}
		
		//otherwise create a filtered array
		//containing only the nodes that pass the conditions
		//conditions are defined in an object literal of key-value
		//pairs relating to attributes, eg {'class':'corner'}
		//will only match elements where the "class" attribute matches "corner"
		//(as a regex, to allow or multiple values and more complex conditions)
		var filtered = [];
		for(var i=0; i<nodes.length; i++)
		{
			var add = true;
			for(var c in conditions)
			{
				if(!conditions.hasOwnProperty(c)) { continue; }
				
				var unmatch = false;
				if(c.charAt(0) == '!')
				{
					unmatch = true;
					c = c.substr(1, c.length - 1);
				}
				
				var attr = c == 'class' ? nodes[i].className : nodes[i].getAttribute(c);
				if(attr == null || attr == '' || new RegExp('\\b(' + conditions[c] + ')\\b', '').test(attr) == unmatch)
				{
					add = false;
				}
			}
			if(add == true)
			{
				filtered.push(nodes[i]);
			}
		}
		return filtered;
	}
};





//convert a DOM node list to an array
Helper.prototype.nodeListToArray = function(nodelist)
{
	var ary = [];
	for(var i=0, len = nodelist.length; i < len; i++)
	{
		ary.push(nodelist[i]);
	}
	return ary;
};



//similar to PHP's empty() function, but more strongly typed
Helper.prototype.empty = function(data)
{
	if(typeof data == 'string' && this.trim(data) === '') { return true; }
	else if(typeof data == 'object')
	{
		if(data instanceof Array && data.length == 0) { return true; }
		else
		{
			var n = 0;
			for(var i in data)
			{
				if(!data.hasOwnProperty(i)) { continue; }
				n++;
			}
			if(n == 0) { return true; }
		}
	}
	return false;
};



//trim leading and trailing whitespace from a string
Helper.prototype.trim = function(str)
{
	return str.replace(/^\s+|\s+$/g,"");
};



//check if an element contains a class name pattern
Helper.prototype.hasClass = function(element, pattern)
{
	return element.className && new RegExp('\\b' + pattern + '\\b').test(element.className);
};


//remove a class name pattern from an element
Helper.prototype.removeClass = function(element, pattern)
{
	element.className = element.className.replace(new RegExp('\\b' + pattern + '\\b', 'g'), '');
	return element;
};



//qualify an href
Helper.prototype.qualifyHREF = function(href, context)
{
	//get the current document location href
	var here = document.location.href;

	//look for a base element to use instead
	var bases = document.getElementsByTagName('base');
	if(bases.length > 0)
	{
		var basehref = bases[0].getAttribute('href');
		if(basehref && basehref != '')
		{
			here = basehref;
		}
	}

	//if the context argument is present and non-empty string, use that instead
	if(typeof context == 'string' && context != '')
	{
		here = context;
	}

	//extract the protocol, host and path
	//and create a location object with the data
	var parts = here.replace('//', '/').split('/');
	var loc = {
		'protocol' : parts[0],
		'host' : parts[1]
		}
	parts.splice(0, 2);
	loc.pathname = '/' + parts.join('/');

	//build a base URI from the protocol plus host (which includes port if applicable)
	var uri = loc.protocol + '//' + loc.host;

	//if the input path is relative-from-here
	//just delete the ./ token to make it relative
	if(/^(\.\/)([^\/]?)/.test(href))
	{
		href = href.replace(/^(\.\/)([^\/]?)/, '$2');
	}

	//if the input href is already qualified, copy it unchanged
	if(/^([a-z]+)\:\/\//.test(href))
	{
		uri = href;
	}

	//or if the input href begins with a leading slash, then it's base relative
	//so just add the input href to the base URI
	else if(href.substr(0, 1) == '/')
	{
		uri += href;
	}

	//or if it's an up-reference we need to compute the path
	else if(/^((\.\.\/)+)([^\/].*$)/.test(href))
	{
		//get the last part of the path, minus up-references
		var lastpath = href.match(/^((\.\.\/)+)([^\/].*$)/);
		lastpath = lastpath[lastpath.length - 1];

		//count the number of up-references
		var references = href.split('../').length - 1;

		//get the path parts and delete the last one (this page or directory)
		var parts = loc.pathname.split('/');
		parts = parts.splice(0, parts.length - 1);

		//for each of the up-references, delete the last part of the path
		for(var i=0; i<references; i++)
		{
			parts = parts.splice(0, parts.length - 1);
		}

		//now rebuild the path
		var path = '';
		for(i=0; i<parts.length; i++)
		{
			if(parts[i] != '')
			{
				path += '/' + parts[i];
			}
		}
		path += '/';

		//and add the last part of the path
		path += lastpath;

		//then add the path and input href to the base URI
		uri += path;
	}

	//otherwise it's a relative path,
	else
	{
		//calculate the path to this directory
		path = '';
		parts = loc.pathname.split('/');
		parts = parts.splice(0, parts.length - 1);
		for(var i=0; i<parts.length; i++)
		{
			if(parts[i] != '')
			{
				path += '/' + parts[i];
			}
		}
		path += '/';

		//then add the path and input href to the base URI
		uri += path + href;
	}

	//return the final uri
	return uri;
};





//add an event listener to an element
//there' no downgrade for older browsers here
//so if encapsulated listeners aren't supported
//the event won't be listened to at all
Helper.prototype.listen = function(target, etype, fn)
{
	if(typeof target.addEventListener != 'undefined')
	{
		target.addEventListener(etype, fn, false);
	}
	else if(typeof target.attachEvent != 'undefined')
	{
		target.attachEvent('on' + etype, fn);
	}
};



//contains method evaluates whether one node contains the other
//as in "does primary node contain event target"
Helper.prototype.contains = function(primary, target)
{
	if(target == primary) { return true; }
	if(target == null) { return false; }
	else { return this.contains(primary, target.parentNode); }
};




//get the top position of an element
Helper.prototype.getTopPosition = function(element)
{
	var top = element.offsetTop;
	element = element.offsetParent;
	while(element != null)
	{
		top += element.offsetTop;
		element = element.offsetParent;
	}
	return top;
};



//get the scrolling top of the page
Helper.prototype.getScrollingTop = function()
{
	if(typeof window.pageYOffset != 'undefined')
	{
		return window.pageYOffset;
	}
	else
	{
		return document.documentElement.scrollTop;
	}
};



//get the canvas height
Helper.prototype.getCanvasHeight = function()
{
	if(typeof window.innerHeight != 'undefined')
	{
		var height = window.innerHeight;
	}
	else
	{
		height = document.documentElement.offsetHeight;
	}

	return height;
};



//create an element and attributes
Helper.prototype.element = function(tagname, attrs)
{
	//create the element with the specified tag name
	var element = document.createElement(tagname);
	
	//if we have any attributes defined
	if(typeof attrs != 'undefined')
	{
		//iterate through the attributes
		for(var i in attrs)
		{
			//ignoring non-string values (such as external prototypes)
			if(typeof attrs[i] != 'string') { continue; }
			
			//switch according to attribute type
			switch(i)
			{
				//for a text node, create the text node inside the element
				case '#text' : 
					element.appendChild(document.createTextNode(attrs[i])); 
					break;
					
				//for a class name, create the attribute using the property name
				//so that it's safe for internet explorer
				case 'class' : 
					element.className = attrs[i];
					
				//for any other attribute create it as normal
				default: 
					element.setAttribute(i, attrs[i]);
			}
		}
	}
	
	//return the created element
	return element;
};




//create a request object
Helper.prototype.createRequestObject = function()
{
	//we should test for the window object first
	//because mac/ie5 returns a [useless] object for activeXobject
	var request = null;
	if(typeof window.XMLHttpRequest != 'undefined')
	{
		try { request = new XMLHttpRequest(); }
		catch(err) { request = null; }
	}
	else if(typeof window.ActiveXObject != 'undefined')
	{
		try { request = new ActiveXObject('Microsoft.XMLHTTP'); }
		catch(err) { request = null; }
	}

	//return the request object / null
	return request;
};



//make an ajax request
Helper.prototype.request = function(method, uri, postdata, oncomplete, onfail)
{
	//reference to this
	var self = this;
	
	//try to create a request object
	var request = this.createRequestObject();

	//if that was successful
	if(request)
	{
		//dev: add some latency for flow control
		//the dev condition will have to be expressed through timeout time
		//since we can't wrap a condition arround "setTimeout(function(){"
		setTimeout(function()
		{
			//open the request, including a timestamp to prevent caching
			//and the ajax flag to identify this request type of PHP
			//[which in turn is used to work out file paths]
			//it did occur to me that I cold pass that identifier as a header value instead
			//but then i'm like, meh, why piss about being clever when this works fine,
			//and in any case is more flexible, eg it can also be used in a <script src>
			request.open(method, uri + (uri.indexOf('?') == -1 ? '?' : '&') 
				+ 'timestamp=' + new Date().getTime() + '&ajax=1', true); 
	
			//if the method is POST
			if(method == 'POST')
			{
				//set request header accordingly
				request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
			}
	
			//if we have a success callback defined
			if(typeof oncomplete == 'function')
			{
				//bind readystatechange handler
				//to call the success and failure callback functions
				request.onreadystatechange = function()
				{
					//when the request has completed we have the response data we need
					if(request.readyState == 4)
					{
						//if the status is zero then either we have no network, 
						//or something's fucked up in the PHP and hence probably my fault
						if(request.status == 0)
						{
							//call the onfail method, passing manual status information
							onfail('Internal Error', 0);
						}
						
						//or if we get a successful response, 
						//fire oncomplete with the trimmed response text
						else if(/^(200|304)$/.test(request.status.toString()))
						{
							oncomplete(self.trim(request.responseText));
						}
						
						//otherwise there's a problem
						//so call the onfail method, passing the status text and code
						else 
						{
							onfail(self.trim(request.statusText), request.status);
						}
					}
				}
			}
	
			//make the request
			request.send(postdata);
			
		//***DEV add some latency in devmode
		}, (self.manager.devmode === true ? 1000 : 0));
	}

	//return the request object
	return request;
};

// DTH1.6 :: Date Time Helper 
//***************************************************************************
// DOM scripting by brothercake -- http://www.brothercake.com/
// (c) SitePoint Pty Ltd. -- http://www.sitepoint.com/ -- All Rights Reserved
//***************************************************************************





//sitepoint datetime helper object
function DateTimeHelper()
{
}


//get all elements of a particular type, within a given context, with a particular microformat classname
//IN# element = string; tag name or "*"
//IN# context = string or null; ID of context element, or null to mean document
//IN# classname = string; className of microformat
//OUT# collection = array; collection of found elements
DateTimeHelper.prototype.get_elements = function(element, context, classname)
{
	//if context is null, use document, otherwise use specified element
	context = context == null ? document : this.manager.helper.get('#' + context, document);

	//get all elements of specified type within the given context
	var elements = this.manager.helper.get(element, context);

	//build a collection of elements containing the given classname
	var collection = [];
	for(var i=0; i<elements.length; i++)
	{
		if(elements[i].className.indexOf(classname) != -1)
		{
			//create an object containing the element reference
			var obj = { 'element' : elements[i] }

			//add data according to the microformat
			switch(true)
			{
				//for datetime, get RFC timestamp from node value
				case (classname.indexOf('sitepoint-datetime') != -1) :
					obj.timestamp = elements[i].firstChild.nodeValue;
					break;
			}

			//add this object to the collection
			collection[collection.length] = obj;
		}
	}

	//return the collection
	return collection;
};


//convert an RFC date string to the same string in local time
//IN# datestring = string; RFC date string
//IN# showseconds = boolean; whether to include seconds in the output
//** could replace this with a resolution argument, eg "minute" means down to minutes, "hour" means down to hours
//IN# compress = boolean; whether to convert very recent dates to word comparison and remove time from older dates
//OUT# local = string; RFC date string in local time
DateTimeHelper.prototype.convert_to_local_string = function(datestring, showseconds, compress)
{
	//convert input date to local string
	var local = new Date(datestring).toString();
	
	//alert('before = ' + datestring + '\nafter = ' + local);
	
	//now see if our locale string ends with the year value
	//(which IE does, while others end with offset)
	var matches = /^.*[ ]([0-9]{4})$/.exec(local);
	//if so, save the year value so we can add it back on
	//because parsing out the offset information will remove it
	if(matches)
	{
		var savedyear = matches[matches.length - 1];
	}

	//parse out the timezone/offset information
	local = local.replace(/(([0-9]{2}:){2}[0-9]{2}).*$/, '$1');

	//now see if the resulting string contains the year
	var hasyear = /[ ]([0-9]{4}[ ])/.test(local);
	//if it doesn't (because of the IE difference just now)
	//then insert it back in the right place
	//in this situation we know that the string will end with the time
	//so we can remove that, add the year, then put it back together again
	if(!hasyear)
	{
		matches = local.match(/[ ]([0-9]{2}:[0-9]{2}:[0-9]{2})$/);
		if(matches)
		{
			var savedtime = matches[matches.length - 1];
			local = local.split(' ');
			local.pop();
			local = local.join(' ') + ' ' + savedyear + ' ' + savedtime;
		}
	}
	
	//parse out the seconds if the showseconds argument is false
	if(!showseconds)
	{
		local = local.replace(/([0-9]{2}:[0-9]{2}):[0-9]{2}/, '$1');
	}
	
	//if the compress argument is true
	if(compress)
	{
		//find out whether the input date is in the future or the past
		var between = this.get_time_difference(datestring);
		var isfuture = between.mindiff >= 0;

		//if the date is in the past get the difference between today at midnight and the input date
		//if it's in the future then get the difference between tomorrow at midnight and the input
		var dateobj = new Date();
		if(isfuture)
		{
			dateobj.setTime(dateobj.getTime() + (1000 * 60 * 60 * 24));
		}
		dateobj.setHours(0, 0, 0, 0);
		between = this.get_time_difference(datestring, dateobj);

		//if the difference between the dates is less than a day
		//(and the total difference is less than 48 hours, so that for eg
		//"Jun 10" is not seen as yesterday on "Sep 11")
		if(between.day < 1 && Math.abs(between.mindiff) < 2880)
		{
			//extract just the time from the local time string
			local = local.replace(/(^.*)( [0-9]{2}.*)$/, '$2');

			//if the input date is in the past
			if(!isfuture)
			{
				//if the mindiff between the input date and today at midnight is positive
				//then the input date occured today
				if(between.mindiff >= 0)
				{
					local = this.manager.lang['datetime']['today'].replace('%time', local);
				}

				//or if it's negative then the date occured yesterday
				else
				{
					local = this.manager.lang['datetime']['yesterday'].replace('%time', local);
				}
			}

			//or if the input date is in the future
			else
			{
				//if the mindiff between the input date and tomorrow at midnight is negative
				//then in the input date will occur today
				if(between.mindiff < 0)
				{
					local = this.manager.lang['datetime']['today'].replace('%time', local);
				}

				//or if it's positive then the date will occur tomorrow
				else
				{
					local = this.manager.lang['datetime']['tomorrow'].replace('%time', local);
				}
			}
		}

		//otherwise just remove the time
		else
		{
			local = local.replace(/(^.*)( [0-9]{2}.*)$/, '$1');
		}
	}
	
	//otherwise, add the word "at" before the time
	else
	{
		local = local.replace(/([ ][0-9]{2}:[0-9]{2})/,' ' + this.manager.lang['datetime']['at'] + ' $1');
	}
	
	//add a comma after the day
	local = local.replace(/(mon|tue|wed|thu|fri|sat|sun)/i, '$1,');

	//return the result
	return local;
};


//get the time difference between two dates
//IN# datestring = string; RFC date string
//IN# dateobj = object; object comparison date
//OUT# between = object; object of difference values
DateTimeHelper.prototype.get_time_difference = function(datestring, dateobj)
{
	//create a date object from the date string,
	var date = new Date(datestring);

	//and a date object that represents now
	//unless we have an input date argument, in which case use that
	var now = typeof dateobj == 'undefined' ? new Date() : dateobj;

	//create an object of differences, beginning with a mindiff value
	//which is the total difference in minutes
	//we can use this to work out if a future time has been reached, or a past time is not in the past
	//and to modify behavior when something has just started or is about to end
	var between = { 'mindiff' : (date.getTime() - now.getTime()) / 60000 };

	//the current assumption is that now is earlier than the input date
	//to give the difference from now to the later date
	//but if that's not the case, invert the values
	//so that you'll get the difference from the earlier date to now
	if (now >= date)
	{
		var tmp = date;
		date = now;
		now = tmp;
	}

	//add individual differences to the between object
	between.year = date.getFullYear() - now.getFullYear();
	between.month = date.getMonth() - now.getMonth();
	between.day = date.getDate() - now.getDate();
	between.hour = date.getHours() - now.getHours();
	between.minute = date.getMinutes() - now.getMinutes();
	between.second = date.getSeconds() - now.getSeconds();

	//convert negative second difference to minute difference
	if (between.second < 0)
	{
		between.minute--;
		between.second += 60;
	}

	//convert negative minute difference to hour difference
	if (between.minute < 0)
	{
		between.hour--;
		between.minute += 60;
	}

	//convert negative hour difference to day difference
	if (between.hour < 0)
	{
		between.day--;
		between.hour += 24;
	}

	//convert negative day difference to month difference
	//this one is more complicated because months aren't all the same length
	if (between.day < 0)
	{
		between.month--;
		var ynum = date.getFullYear();

		var mlengths = [
			31,
			(ynum % 4 == 0 && ynum % 100 != 0 || ynum % 400 == 0) ? 29 : 28,
			31, 30, 31, 30, 31, 31, 30, 31, 30, 31
			];

		var mnum = date.getMonth() - 1;
		if (mnum < 0) { mnum += 12; }

		between.day += mlengths[mnum];
	}

	//convert negative month difference to year difference
	if (between.month < 0)
	{
		between.year--;
		between.month += 12;
	}

	//return the between object
	return between;
};


//format the difference between two dates
//IN# between = object; between object returned by get_time_between
//IN# direction = string; "endtime" or "startime" to control the syntax
//IN# seconds = boolean; whether to include seconds in the output
//** could replace this with a resolution argument, eg "minute" means down to minutes, "hour" means down to hours
//IN# abbreviate = boolean; whether to abbreviate time value names in the output, eg. "minute" to "min"
//IN# compress = number; the number of significant factors to compress to; zero means don't compress
//*** NOTE: the compress argument was a boolean in the last version of this class, where true meant a factor of 2
//OUT# sentence = string; formatted sentence for time since / remaining
DateTimeHelper.prototype.format_time_difference = function(between, direction, seconds, abbreviate, compress)
{
	//begin to define the output sentence
	var sentence = '';

	//if the direction is endtime and mindiff is less than 1
	if(direction == 'endtime' && between.mindiff < 1)
	{
		//if it's less than zero this is in an endtime in the past
		//so just return and empty string and we're done
		if(between.mindiff < 0)
		{
			return '';
		}

		//otherwise if we're not including seconds
		//add "less than 1 minute" to the string
		else if(!seconds)
		{
			sentence += this.manager.lang['datetime']['imminent'];
		}
	}

	//or if the direction is starttime and mindiff is greater than -1
	else if(direction == 'starttime' && between.mindiff > -1)
	{
		//if it's greater than zero this is a starttime in the future
		//so just return and empty string and we're done
		if(between.mindiff > 0)
		{
			return '';
		}

		//otherwise if we're not including seconds
		//add "Less than 1 minute" to the string
		else if(!seconds)
		{
			sentence += this.manager.lang['datetime']['imminent'];
		}
	}

	//otherwise iterate through the between object and compile the object
	else
	{
		for(var i in between)
		{
			//don't include mindiff, or seconds if the seconds argument is false
			if(i == 'mindiff' || (i == 'second' && !seconds)) { continue; }
	
			//add this value if it's not zero
			if(between[i] > 0)
			{
				sentence += between[i] + ' ' 
							+ this.manager.lang['datetime']['units'][i] + (between[i] > 1 ? 's' : '') + ', ';
			}
		}
	}
	
	//if the compress argument is present and not zero, split the resulting sentence
	//then re-compile including the specified number of factors
	if(typeof compress != 'undefined' && compress > 0)
	{
		sentence = sentence.split(', ');
		if(sentence.length > compress)
		{
			sentence.length = compress;
		}
		sentence = sentence.join(', ');
	}

	//cut off any trailing comma-space
	if(sentence.charAt(sentence.length - 2) == ',')
	{
		sentence = sentence.substring(0, sentence.length - 2);
	}

	//convert the final comma to " and", if there is one
	if(sentence.indexOf(',') != -1)
	{
		var index = sentence.lastIndexOf(',');
		sentence = sentence.substring(0, index) + sentence.substr(index).replace(',', ' ' + this.manager.lang['datetime']['and'])
	}

	//if the direction is startime, add the final sentence fragment
	if(direction == 'starttime') { sentence += ' ' + this.manager.lang['datetime']['ago']; }

	//if the abbreviate argument is true, abbreviate time value names
	if(abbreviate)
	{
		//define the abbreviations
		var abbr = { 
			'year' : this.manager.lang['datetime']['shortunits']['year'], 
			'month' : this.manager.lang['datetime']['shortunits']['month'], 
			'hour' : this.manager.lang['datetime']['shortunits']['hour'], 
			'minute' : this.manager.lang['datetime']['shortunits']['minute'], 
			'second' : this.manager.lang['datetime']['shortunits']['second'] 
			};

		//iterate through the abbreviations
		//and do string replacements on the sentence
		for(var a in abbr)
		{
			sentence = sentence.replace(a, abbr[a]);
		}
	}

	//return the formatted sentence
	return sentence;
};




//get the user's timezone offset in RFC format (eg. "+1000")
//OUT# rfc = string; formated version of timezone offset
DateTimeHelper.prototype.get_rfc_timezoneoffset = function()
{
	//get the offset (between GMT and locale) and divide into hours
	var offset = new Date().getTimezoneOffset() / 60;

	//begin compiling the output string
	var rfc = '';

	//if we're not at GTM
	if(offset != 0)
	{
		//a positive offset is behind GMT
		if(offset > 0)
		{
			rfc += '-';
		}
		//a negative offset is ahead of GMT
		else
		{
			offset = Math.abs(offset);
			rfc += '+';
		}

		//add trailing zero if necessary
		if(offset < 10) { rfc += '0'; }

		//add fractional offset, converted into minutes if necessary
		if(parseInt(offset, 10) != offset)
		{
			rfc += '' + parseInt(offset, 10) + ((offset - parseInt(offset, 10)) * 60);
		}

		//otherwise just add trailing zeros
		else
		{
			rfc += offset + '00';
		}
	}

	//otherwise we're GMT
	else
	{
		rfc += '+0000';
	}

	//return the result
	return rfc;
};




//convert a JS generated date so it matches the RFC format used by PHP
//IN# datestring = string; JS generated date string
//OUT# datestring = string; RFC format date string
DateTimeHelper.prototype.convert_js_date = function(datestring)
{
	return datestring.replace(/^([a-z]{3})/i, '$1,')
	                 .replace(/(GMT).*$/, 'GMT')
	                 .replace(/([a-z]{3})[ ]([0-3][0-9])/i, '$2 $1');
};



// FSM1.0 :: Flippa Site Manager
//***************************************************************************
// (c) SitePoint Pty Ltd. -- http://www.sitepoint.com/ -- All Rights Reserved
//***************************************************************************


jQuery(document).ready(function() {
	
	jQuery("#q").bind("focus", function() {
		if (jQuery(this).val() == "Keywords") {
			jQuery(this).val("").addClass("modified");
		}
	});
	
	jQuery("#q").bind("blur", function() {
		if (jQuery(this).val() == "" && jQuery(this).hasClass("modified")) {
			jQuery(this).val("Keywords").removeClass("modified");
		}
	});
	
});



//site manager class is included site-wide
//and controls all other scripted functionality
function SiteManager(config, lang)
{
	//weed out unsupported browsers
	if(typeof document.getElementById == 'undefined' || typeof document.createElement == 'undefined') { return; }

	//save the config and language objects
	this.config = config;
	this.lang = lang;
	
	//convert devmode to boolean and save to manager property
	this.devmode = this.config.devmode == 'yes';
	
	//identify some browsers that need tweaking or bugfixing
	this.msie = typeof document.uniqueID != 'undefined';
	this.msie6 = this.msie && /msie 6/i.test(navigator.userAgent);
	this.msie8 = this.msie && /msie 8/i.test(navigator.userAgent);
	this.safari = navigator.vendor == 'Apple Computer, Inc.';
	this.chrome = navigator.vendor == 'Google Inc.';
	this.firefox = navigator.product == 'Gecko' && !this.safari && !this.chrome;
	this.opera = typeof window.opera != 'undefined';

	//instantiate the helper class and save the reference
	//sending a manager reference to the class for cross communication
	this.helper = new Helper();
	this.helper.manager = this;
	
	//enable state-scope image replacement
	//http://www.sitepoint.com/article/image-replacement-state-scope/
	this.helper.enableStateScopeImageReplacement();
	
	//if we have a notice bar, add its close button
	//this.addNoticeBarCloseButton();
	
	//if this is IE, but not IE8
	if(this.msie && !this.msie8)
	{
		//apply how-it-works numbering, if applicable
		this.applyWorksNumbering();
	}

	//if we have the datetime helper class
	if(typeof DateTimeHelper != 'undefined')
	{
		//instantiate the datetime helper class
		this.datetime = new DateTimeHelper();
	
		//send a manager reference for cross communication
		this.datetime.manager = this;

		//check the contextIDs to make sure we have them
		var contexts = [];
		for(var i=0; i<this.config['manager']['contexts'].length; i++)
		{
			if(this.helper.get('#' + this.config['manager']['contexts'][i]))
			{
				contexts.push(this.config['manager']['contexts'][i]);
			}
		}

		//for each context, convert sitepoint microformatted datetimes	
		for(i=0; i<contexts.length; i++)
		{
			this.convertDateTimeMicroformats(contexts[i]);
		}
	}	
	
	//if we have the form helper class
	if(typeof FormsHelper != 'undefined')
	{
		//instantiate it 
		this.forms = new FormsHelper();
			
		//send a manager reference for cross communication
		this.forms.manager = this;

		//initialize
		this.forms.init();
	}

	//if we have the homepage class
	if(typeof Homepage != 'undefined')
	{
		//instantiate it 
		this.homepage = new Homepage();
			
		//send a manager reference for cross communication
		this.homepage.manager = this;

		//initialize
		this.homepage.init();
	}
	
	//if we have the beatbox class
	if(typeof BeatBox != 'undefined')
	{
		//instantiate it with config parameters
		this.beatbox = new BeatBox(this.config['beatbox']['maxsize'], this.config['beatbox']['thumbnails']);

		//send a manager reference for cross communication
		this.beatbox.manager = this;
	}
	
	//if we have the statsboxes class
	if(typeof StatsBoxes != 'undefined')
	{
		//instantiate it 
		this.statsboxes = new StatsBoxes();
			
		//send a manager reference for cross communication
		this.statsboxes.manager = this;

		//initialize
		this.statsboxes.init();
	}

	//if we have the search results class
	if(typeof SearchResults != 'undefined')
	{
		//instantiate it 
		this.results = new SearchResults();
			
		//send a manager reference for cross communication
		this.results.manager = this;

		//initialize
		this.results.init();
	}
}







//if we have a notice bar, add its close button
SiteManager.prototype.addNoticeBarCloseButton = function()
{
	//get a reference to the noticerbar, and check it's there
	var noticebar = this.helper.get('#notice-bar');
	if(!noticebar) { return; }
	
	//add the closer element
	var closer = this.helper.element('a', {
		'href' : 'javascript:void("' + this.lang['noticebar']['label'] + '")', 
		'class' : 'close',
		'#text' : ' ' + this.lang['noticebar']['label'], 
		'title' : this.lang['noticebar']['tooltip']
		});
		
	//remove the noticebar on closer element click, 
	//using fast fade and shrink animations to be cute
	//but it's too dog slow and kludgy in IE < 8, so just take it away
	var self = this;
	this.helper.listen(closer, 'click', function(e)
	{
		if(self.msie6)
		{
			noticebar.parentNode.removeChild(noticebar);
		}
		else
		{
			self.helper.fade(self.helper.get('p', noticebar)[0], 1);
			self.helper.fade(noticebar, 3);
			self.helper.shrink(noticebar, 3, function()
			{
				noticebar.parentNode.removeChild(noticebar);
			});
		}
	
		try { e.preventDefault(); } catch(err) {} finally { return false; }
	});

	//add the created closer element to the noticebar
	this.helper.get('p', noticebar)[0].appendChild(closer);
};







//apply how-it-works numbering in IE
SiteManager.prototype.applyWorksNumbering = function()
{
	var works = this.helper.get('#how-it-works');
	if(!works) { return; }
	
	var items = this.helper.get('h2', works, {'class':'h3'});
	for(var i=0; i<items.length; i++)
	{
		items[i].innerHTML = (i + 1) + '. ' + items[i].innerHTML;
	}
};






//convert sitepoint microformatted datetimes	
SiteManager.prototype.convertDateTimeMicroformats = function(contextID)
{
	//whether to add title attributes to converte datetimes spans
	//that contains the original timestamp, converted to local time
	var addtitles = this.config['manager']['addtitles'] == 'yes';
	
	//get a collection of contest times, which are span elements within the context
	//that have the sitepoint-datetime microformat classname
	//this will return an array of objects with .element and .timestamp properties
	var times = this.datetime.get_elements('span', contextID, 'sitepoint-datetime');

	//if we have no elements, we're done here
	if(times.length == 0) { return; }
	
	//***DEV if we're in devmode, create useful timestamps
	if(this.devmode === true)
	{
		times = this.createSampleTimestamps(contextID, times);
	}

	//iterate through the collection
	for(var i=0; i<times.length; i++)
	{
		//if the item is a starttime
		if(times[i].element.className.indexOf('starttime') != -1)
		{
			//convert the time to a local datetime string without seconds
			//and for most situations we want to compress the date to a summary version 
			//if applicable (ie. "today"), but not for auction questions or auction add question
			//for which we just want the display the date as it is
			var compress = !/auction\-(questions|add\-question)/.test(contextID);
			var time = this.datetime.convert_to_local_string(times[i].timestamp, false, compress);
					
			//if this is a listings table of any kind, apart from the pm inbox
			if(/\-table$/.test(contextID) && contextID != 'inbox-table')
			{
				//if the output is "today" or "yesterday", strip out the time
				if(/(today|yesterday)/i.test(time))
				{
					time = time.replace(/^(today|yesterday).*/i, '$1');
				}
				//otherwise strip out the day and year
				else
				{
					time = time.split(' ');
					time = time[1] + ' ' + time[2];
				}
			}
			
			//if this is the pm inbox table or message details list
			else if(/inbox\-table|message\-details/.test(contextID))
			{
				//if the message date ends in this year, strip it out
				var thisyear = new Date().getFullYear(), regyear = new RegExp('[ ]' + thisyear + '$');
				if(regyear.test(time))
				{
					time = time.replace(regyear, '');
				}
				
				//remove the day from the start of the date, if present
				time = time.replace(/^(mon|tue|wed|thu|fri|sat|sun)(, )/i, '');
			}
			
			//if this is the auction history info 
			else if(contextID == 'auction-history')
			{
				//if this is the auction history info, add how long ago it was in brackets
				if(contextID == 'auction-history')
				{
					//get the difference between now and the endtime
					var between = this.datetime.get_time_difference(times[i].timestamp);
					
					//for formatting the difference, we only want one significant factor (days)
					//unless the auction was started within the last day, 
					//in which case we want two factors (hours and minutes) for accuracy
					var factors = (between.month == 0 && between.day == 0) ? 2 : 1;
	
					//format this difference as a sentence, without seconds, 
					//abbreviating the time units cos we're short on space here
					//and compressing the difference down to the specified significant factors
					var sentence = this.datetime.format_time_difference(between, 'starttime', false, true, factors);
				
					//add the formatted difference sentence to the output time
					time += ' <em>(' + sentence + ')</em>';
				}
			}
			
			//if this is the auction questions or auction add question
			else if(/auction\-(questions|add\-question)/.test(contextID))
			{
				//strip out the time
				time = time.split(new RegExp(' ' + this.lang['datetime']['at'] + ' '))[0];
			}
			
			
			//if this isn't the auction history info
			//write a combination of the local datetime string and the timestamp, 
			//converted to local time, as a title attribute to the element
			if(contextID != 'auction-history' && addtitles)
			{
				times[i].element.setAttribute('title', time + '  [' + new Date(times[i].timestamp).toLocaleString() + ']');
			}
			
			//if we have a date and a year (but not the day) seperate them with a comma
			var dateplusyear = new RegExp('([a-z]{3})[ ]([0-9]{2})[ ]([0-9]{4})', 'i');
			if(dateplusyear.test(time) && !/(mon|tue|wed|thu|fri|sat|sun)/i.test(time))
			{
				time = time.replace(dateplusyear, '$1 $2, $3');
			}
			
			//write the local datetime string back to the source element
			times[i].element.innerHTML = time;
		}
	}

	//reference to this
	var self = this;

	//wrapper function to convert endtime to time difference countdown
	function convert_endtime()
	{
		//iterate through the collection
		for(var i=0; i<times.length; i++)
		{
			//if the item is an endtime and doesn't have the disabled class
			if(times[i].element.className.indexOf('endtime') != -1 
				&& times[i].element.className.indexOf('disabled') == -1)
			{
				//convert the time to a local datetime string without seconds
				//also compressing dates to summary versions
				var time = self.datetime.convert_to_local_string(times[i].timestamp, false, true);

				//get the difference between now and the endtime
				var between = self.datetime.get_time_difference(times[i].timestamp);

				//format this difference as a sentence, without seconds, not abbreviated
				//and compressing the difference down to 2 significant factors
				var sentence = self.datetime.format_time_difference(between, 'endtime', false, false, 2);

				//if the context is "bid-overview" we want a descriptive longhand sentence with time
				if(contextID == 'bid-overview')
				{
					//get the longhand sentence
					var description = self.datetime.convert_to_local_string(times[i].timestamp, false, false);
					
					//if we have a date and a year (but not the day) seperate them with a comma
					var dateplusyear = new RegExp('([a-z]{3})[ ]([0-9]{2})[ ]([0-9]{4})', 'i');
					if(dateplusyear.test(description) && !/(mon|tue|wed|thu|fri|sat|sun)/i.test(description))
					{
						description = description.replace(dateplusyear, '$1 $2, $3');
					}
					
					//if the sentence var was empty then the auction has ended
					if(sentence == '')
					{
						//wrap <del> around the description so we have semantic indication without author CSS
						description = '<del>' + description + '</del>';

						//add the longhand "ended" sentence to the description
						description +=' <em>(' + self.lang['datetime']['long-ended'] + ')</em>';

						//remove the imminent class names from the datetime span
						times[i].element.className = times[i].element.className.replace(/[ ]?(super\-)?imminent/g, '');

						//add the disabled class
						times[i].element.className += ' disabled';

						//if we have bid or bin buttons, disable them	
						var bid = self.helper.get('#placebid');
						if(bid)
						{
							bid.className += ' disabled';
							bid.disabled = true;
						}
						var bin = self.helper.get('#buyitnow');
						if(bin)
						{
							bin.className += ' disabled';
							bin.disabled = true;
						}
					}
					
					//otherwise the auction is still active
					else
					{
						//add the sentence to the description
						description += ' <em>(' + sentence + ')</em>';

						//if there's 2 hours or less to go add the imminent class name
						//(using 121 because it might be 2 hours and 59 seconds, which displays as 2 hours)
						if(between.mindiff < 121)
						{
							times[i].element.className += ' imminent';

							//and wrap a <strong> around the description so we have semantic indication without author CSS
							description = '<strong>' + description + '</strong>';
						}
						
						//if there's two minutes or less to go, add the super-imminent class name
						//(using less than 3 because it might be 2 minutes and 59 seconds, which display as 2 minutes)
						if(between.mindiff < 3)
						{
							times[i].element.className += ' super-imminent';
						}
					}

					//write the description back to the source element
					times[i].element.innerHTML = description;
				}
				
				//for any other context (rem: apart from bid-overview)
				else
				{
					//if the sentence value is empty then the auction has ended
					if(sentence == '')
					{
						//define the message, using the no-time phrase for auction history
						//or the shorthand ended phrase for any other situation
						var msg = self.lang['datetime'][(contextID == 'auction-history' ? 'no-time' : 'ended')];
						
						//write the message back to the source element
						times[i].element.innerHTML = msg;
	
						//remove the imminent class name 
						times[i].element.className = times[i].element.className.replace(/[ ]?(super\-)?imminent/g, '');

						//unless this is the "recently-sold" table
						if(!self.helper.hasClass(self.helper.get('#' + contextID), 'recently\-sold'))
						{
							//add the disabled className
							times[i].element.className += ' disabled';

							//and wrap <del> around the text so we have semantic indication without author CSS
							times[i].element.innerHTML = '<del>' + times[i].element.innerHTML + '</del>';
						}
						
						//if we have bid or bin buttons, disable them	
						var bid = self.helper.get('#placebid');
						if(bid)
						{
							bid.className += ' disabled';
							bid.disabled = true;
						}
						var bin = self.helper.get('#buyitnow');
						if(bin)
						{
							bin.className += ' disabled';
							bin.disabled = true;
						}

						//write a combination of the message and the timestamp, 
						//converted to local time, as a title attribute to the element
						if(addtitles)
						{
							times[i].element.setAttribute('title', msg + '  [' + new Date(times[i].timestamp).toLocaleString() + ']');
						}
	
						//look for a containing table row, by iterating upwards until we find one 
						//or breaking if we get to the body element
						var node = times[i].element;
						while(node.nodeName.toLowerCase() != 'tr')
						{
							if(!node.parentNode) { break; }
							else if(node.nodeName.toLowerCase() == 'body') { break; }
							else { node = node.parentNode; }
						}
	
						//if we have a row
						if(node.nodeName.toLowerCase() == 'tr')
						{
							//unless this is the "recently-sold" table
							if(!self.helper.hasClass(self.helper.get('#' + contextID), 'recently\-sold'))
							{
								//add the disabled class name
								node.className += ' disabled';
								
								//if the row contains a header cell with rowspan=2, disable the next row too
								//this will happen on the buy page, which uses two rows per listing
								var th = self.helper.get('th', node, {'rowspan':'2'});
								if(th[0])
								{
									var nextrow = node.nextSibling;
									while(nextrow.nodeType != 1)
									{
										nextrow = nextrow.nextSibling;
									}
									nextrow.className += ' disabled';
								}
							}
						}
					}
	
					//otherwise the auction is still active
					else
					{
						//if this is a listings table of any kind
						if(/\-table$/.test(contextID))
						{
							//if the total difference is greater than a day
							//split the sentence up and remove the hours
							//so we only get a rounded difference in days
							//or if the total difference is less than a day
							//but more than an hour, split off the hours
							if(
								(between.mindiff > (60 * 24))
								||
								((between.mindiff < (60 * 24) && between.mindiff > 60))
								)
							{	
								sentence = sentence.split(self.lang['datetime']['and']);
								sentence = self.helper.trim(sentence[0]);
							}
						}
						
						//write the difference sentence back to the source element
						times[i].element.innerHTML = sentence;
	
						//write a combination of the difference sentence and the timestamp, 
						//converted to local time, as a title attribute to the element
						if(addtitles)
						{
							times[i].element.setAttribute('title', sentence + '  [' + new Date(times[i].timestamp).toLocaleString() + ']');
						}
						
						//if there's 2 hours or less to go add the imminent class name
						//(using 121 because it might be 2 hours and 59 seconds, which displays as 2 hours)
						if(between.mindiff < 121)
						{
							times[i].element.className += ' imminent';
							
							//and wrap <em> around the text so we have semantic indication without author CSS
							//(can't use strong here because it's already used to meaning something else)
							times[i].element.innerHTML = '<em>' + times[i].element.innerHTML + '</em>';
						}
						
						//if there's two minutes or less to go, add the super-imminent class name
						//(using less than 3 because it might be 2 minutes and 59 seconds, which display as 2 minutes)
						if(between.mindiff < 3)
						{
							times[i].element.className += ' super-imminent';
						}
					}
				}
			}
		}
	}

	//do the end time conversion straight away
	convert_endtime();

	//then repeat it approximately every 30 seconds
	//even though the output resolution is only to the minute
	//"1 min" could mean anything from 1:01 to 1:59
	//so this improves the overall accuracy of the data for a given view
	window.setInterval(function() { convert_endtime(); }, 30000);
};








//***DEV FUNCTION creates useful timestamps wherever datetime microformats are used
//to save having to continually edit the sample data manually
SiteManager.prototype.createSampleTimestamps = function(contextID, times)
{
	//get a reference to the context element, and save the number of milliseconds in one day
	var context = this.helper.get('#' + contextID), oneday = 86400000;
	
	//iterate through the times microformats
	for(var i=0; i<times.length; i++)
	{
		//create variables that say: whether to create a date in the future or the past, 
		//how many constant days to add, how many variable (ie. randomized) days to add
		var future = true, consts = 0, vars = 0;
		
		//for "ending soon" homepage table, create an end date up to one day in the future
		//and a start date between 15 and 20 days in the past
		if(this.helper.hasClass(context, 'ending\-soon'))
		{
			future = times[i].element.className.indexOf('endtime') != -1;
			if(future) { vars = 1; }
			else { consts = 15; vars = 5; }
		}
		//for "recently sold" homepage table, create an end date up to 10 days in the future
		//and a start date between 10 and 30 days in the past
		else if(this.helper.hasClass(context, 'recently\-sold'))
		{
			future = false;
			if(times[i].element.className.indexOf('endtime') != -1) { vars = 10; }
			else { consts = 10; vars = 20; }
		}
		//for pm message details, either create a date within 24 hours, or a date up to 6 months ago (50/50 chance)
		else if(this.helper.hasClass(context, 'message\-details'))
		{
			future = false;
			if(Math.random() * 2 > 1) { vars = 1; }
			else { vars = 180; }
		}
		//for auction history
		else if(this.helper.hasClass(context, 'auction\-history'))
		{
			//if this is the starttime, create a date up to 10 days in the past, but at least an hour ago
			if(times[i].element.className.indexOf('starttime') != -1) 
			{ 
				future = false; 
				consts = (1 / 24);  
				vars = 10; 
			}

			//if this is the endtime, create a date up to 7 days in the future
			else { vars = 7; }
		}
		//for auction questions
		else if(this.helper.hasClass(context, 'auction\-questions'))
		{
			//create dates in chronological order: 0-2, 3-6, 7-12, and 13-21 days ago
			future = false;
			switch(i)
			{
				case 0 : vars = 2; break;
				case 1 : consts = 3; vars = 4; break;
				case 2 : consts = 7; vars = 6; break;
				case 3 : consts = 13; vars = 9; break;
			}
		}
		//for auction add a question
		else if(this.helper.hasClass(context, 'auction\-add\-question'))
		{
			//we want the time and date to be current
		}
		//for the pm inbox 
		else if(this.helper.hasClass(context, 'inbox'))
		{
			future = false;
			switch(i)
			{
				//for the first message create a date up to 24 hours ago
				case 0 : vars = 1; break;
					
				//for the second and third message create dates between three days and two months in the past
				case 1 : case 2 : consts = 3; vars = 60; break;
					
				//for the final message create a date at least six months ago (so it comes out as last year)
				case 3 : consts = 180; vars = 31; break;						
			}
		}
		//for the listings table on the buy page, if this is the 3rd or 7th listing 
		else if(this.helper.hasClass(this.helper.get('div')[3], 'buy') && (i == 4 || i == 12))
		{
			//create a date between 5 and 10 days in the past so that it shows as ended
			future = false;
			consts = 5;
			vars = 10;
		}
		//for anything else
		else
		{
			//for an endtime, create a date up to 20 days in the future
			//for a starttime, create a date between 5 and 20 days in the past
			future = times[i].element.className.indexOf('endtime') != -1;
			if(future) { vars = 20; }
			else { consts = 5; vars = 15; }
		}
		
		
		//create the date with the specified difference
		var devdate, now = new Date().getTime();
		if(future)
		{
			devdate = new Date(now + (oneday * consts) + (oneday * (Math.random() * vars))).toString();
		}
		else
		{
			devdate = new Date(now - (oneday * consts) - (oneday * (Math.random() * vars))).toString();
		}

		//convert to RFC format and write it back to the datetime span
		times[i].element.innerHTML = this.datetime.convert_js_date(devdate);
	}
	
	//re-get and return the microformats collection with this new data
	return this.datetime.get_elements('span', contextID, 'sitepoint-datetime');
};









//DOM cleaner for IE
if(typeof window.attachEvent != 'undefined')
{
	//window unload listener
	window.attachEvent('onunload', function()
	{
		//relevant events
		var ev = ['click','load','error','mouseover','mouseout','focus','blur','mousemove','mousedown'];
		var el = ev.length;

		//for each item in the document.all collection
		var dl = document.all.length;
		for(var i=0; i<dl; i++)
		{
			//for each relevant event
			for(var j=0; j<el; j++)
			{
				//set it to null so it's garbage collected
				document.all[i]['on' + ev[j]] = null;
			}
		}
	});
}


// *********************************
// SITE ALERTS
// ********************************

var SiteAlerts = {
	
	'hideAlert' : function (k)
	{
		//Track this in a cookie
		var hiddenIds = [];
		
		var cookieBits = document.cookie.split(/\s*;\s*/g);
		for (var i in cookieBits)
		{
			var nvp = cookieBits[i].substring(9).split('=');
			var key = unescape(nvp[0]);
			var value = unescape(nvp[1]);
			
			if (key == "hiddenalerts")
			{
				hiddenIds = value.split(',');
			}
		}
		
		hiddenIds.push(k);
		
		var date = new Date();
		date.setTime(date.getTime() + (999 * 24 * 60 * 60 * 1000));
		
		document.cookie = "hiddenalerts=" + hiddenIds.join(',') +
			"; expires=" + date.toGMTString() +
			"; path=/";
		//End cookie tracking
		
		jQuery("#site_alert_" + k).remove();
		
		var alertRow = document.getElementById('site_alert_' + k);
		var alertList = document.getElementById('notice-bar');
		//Remove the alert panel if no messages left
		if (alertList && !alertList.getElementsByTagName('p').length)
		{
			jQuery(alertList).hide('fast');
		}
		
		return false;
	}
	
};

// FMH1.0 :: Forms Helper
//***************************************************************************
// (c) SitePoint Pty Ltd. -- http://www.sitepoint.com/ -- All Rights Reserved
//***************************************************************************


//form helper controls form-related functionality
function FormsHelper()
{
}




//initialization method
FormsHelper.prototype.init = function()
{
	//no point if we don't have any forms
	if(this.manager.helper.get('form').length == 0) { return; }
	
	//fix buttons
	this.fixButtons();
	
	//add check/uncheck all functionality to tables with selection checkboxes
	this.addCheckall();
	
	//tweak radio and checkbox positions
	this.tweakRadioAndCheckboxPositions();
	
	//initialize any dependent field/checkbox pairs
	this.initDependentFields();
	
	//if this is IE, but not IE8
	if(this.manager.msie && !this.manager.msie8)
	{
		//apply breadcrumb numbering, if applicable
		this.applyBreadcrumbNumbering();

		//add handlers to implement a focus state
		this.addFormFocusHandlers();
	}
	
	//add handlers for styled form tooltips (for notes and errors), if applicable
	this.addFormTooltipHandlers();
	
	//add handlers to text input fields that clears any default value
	//this.defaultValueHandler();
	
	//initialize dynamic preview, if applicable
	this.initDynamicPreview();
	
	//add show password checkbox, if applicable
	this.addShowPassword();
};







//fix button padding in browsers except firefox, cos they were driving me crazy
//atually firefox is the only one that gets it wrong, and the others were already right
//but i've optimized for firefox because it's the most popular wrong browser
//so it's all the others i've had to end up applying this fix to. sad innit.
FormsHelper.prototype.fixButtons = function()
{
	//if this is not firefox
	if(!this.manager.firefox)
	{
		//get all button-containing labels and iterate through them
		var buttonlabels = this.manager.helper.get('label', document, {'class':'button'});
		for(var i=0; i<buttonlabels.length; i++)
		{
			//look for the the button inside, and if there is one
			var buttons = this.manager.helper.get('input', buttonlabels[i], {'type':'submit|button'});
			if(buttons[0])
			{
				//apply a padding tweak
				buttons[0].style.paddingTop = (this.manager.msie || this.manager.chrome ? 1 : 2) + 'px';
				buttons[0].style.paddingBottom = (this.manager.msie || this.manager.chrome ? 2 : 1) + 'px';
				
				//if this is safari, also convert the value text to uppercase
				//this is for the benefit of safari 3, which doesn't appear 
				//to support text-transform on input elements
				if(this.manager.safari)
				{
					buttons[0].value = buttons[0].value.toUpperCase();
				}
			}
		}
	}
};





//add check/uncheck all functionality to tables with selection checkboxes
FormsHelper.prototype.addCheckall = function()
{
	//dont do this in ie, which doesn't support the creation of input types
	//and anyway tbody referencing won't work either, because IE doesn't recognise it as an element
	if(this.manager.msie) { return; }
	
	//get the collection of tables that can have this functionality
	//and don't continue if there aren't any
	var tables = this.manager.helper.get('table', document, {'class':'selectable'});
	if(tables.length == 0) { return; }
	
	//iterate through them to add the necessary
	for(var i=0; i<tables.length; i++)
	{
		//create a tfoot with the checkbox and label
		var tfoot = this.manager.helper.element('tfoot');
		var tr = tfoot.appendChild(this.manager.helper.element('tr'));
		var td = tr.appendChild(this.manager.helper.element('td', {'colspan':'10'}));
		var checkbox = td.appendChild(this.manager.helper.element('input', {
			'type' : 'checkbox',
			'id' : 'selectall' + i
			}));
		var label = td.appendChild(this.manager.helper.element('label', {
			'for' : 'selectall' + i,
			'#text' : this.manager.lang['select-all']['label']
			}));
			
		//get the existing collection of checkboxes inside this table
		//and save the array as a property of this checkbox
		checkbox.checkboxes = this.manager.helper.get('input', tables[i], {'type':'checkbox'});
			
		//add the finished tfoot to the table, before the tbody
		tables[i].insertBefore(tfoot, this.manager.helper.get('tbody', tables[i])[0]);
		
		//bind a click handler to the <td> to handle clicks inside it
		this.manager.helper.listen(td, 'click', function(e)
		{
			//target reference; no need for target forking because we don't support IE
			var target = e.target;
			
			//convert label reference
			if(/label/i.test(target.nodeName)) { target = target.previousSibling; }
			
			//check or uncheck each of the collection of checkboxes
			//according to whether this checkbox is checked
			for(var j=0; j<target.checkboxes.length; j++)
			{
				target.checkboxes[j].checked = target.checked;
			}
		});
	}
};





//tweak the position of radio buttons and checkboxes to put them in line with their labels
//but not if they're inside inverted fieldrows, because they're floated and hence already in line
//(except radio buttons in firefox, but they're catered for in static css)
FormsHelper.prototype.tweakRadioAndCheckboxPositions = function()
{
	var tweak = 0, inputs = this.manager.helper.get('input', document, {'type':'radio|checkbox'});
	switch(true)
	{
		case this.manager.firefox : tweak = 2; break;
		case this.manager.opera : tweak = -1; break;
		case this.manager.msie8 : tweak = 1; break;
	}
	if(tweak == 0) { return; }
	for(var i=0; i<inputs.length; i++)
	{
		if(this.manager.helper.hasClass(inputs[i].parentNode.parentNode, 'inverted')) { continue; }
		inputs[i].style.top = tweak + 'px';		
	}
};








//initialize any dependent field/checkbox pairs
FormsHelper.prototype.initDependentFields = function()
{
	//look for elements that have a marked dependency
	//identified by "dependency-xxx" in their class name
	//and further qualify them to check that they're fieldrow elements
	var drows = this.manager.helper.get('*', document, {'class':'dependency\-'});
	for(var i=0; i<drows.length; i++)
	{
		if(!this.manager.helper.hasClass(drows[i], 'fieldrow'))
		{
			drows.splice(i--, 1);
		}
	}
	if(drows.length == 0) { return; }
	
	//iterate through the resulting fieldrow elements
	for(i=0; i<drows.length; i++)
	{
		//look for a collection of fields inside the row 
		var dfields = this.manager.helper.get('input', drows[i]).concat(this.manager.helper.get('textarea', drows[i]));
		
		//get a reference to the associated checkbox		
		var idref = drows[i].className.replace(/^.*dependency\-([^ ]+).*$/, '$1');
		var checkbox = this.manager.helper.get('#' + idref);
		
		//if we don't have a checkbox, abandon this instance
		if(!checkbox) { continue; }

		//save the fieldrow and collection of fields references
		//as properties of the checkbox
		checkbox.drow = drows[i];
		checkbox.dfields = dfields;

		//iterate through the collection of fields to disable each one
		for(var j=0; j<dfields.length; j++)
		{
			if (!checkbox.checked)
			{
				dfields[j].disabled = true;
				//apply the disabled class name to the row
				drows[i].className += ' disabled';
			}
		}
		
		//reference to this
		var self = this;
		
		//now bind a click handler to the checkbox 
		//to enable and disable the associated fields
		this.manager.helper.listen(checkbox, 'click', function(e)
		{
			//get the target checkbox reference
			var target = e.target ? e.target : e.srcElement;
			
			//iterate through the associated fields
			//and enable or disable them according to
			//whether this checkbox is now checked or not
			for(var i=0; i<target.dfields.length; i++)
			{
				target.dfields[i].disabled = !target.checked;
			}
			
			//add or remove the disabled class from the fieldrow, likewise
			if(target.checked)
			{
				target.drow = self.manager.helper.removeClass(target.drow, ' disabled');
			}
			else
			{
				target.drow.className += ' disabled';
			}
		});
	}
};






//apply breadcrumb numbering in IE
FormsHelper.prototype.applyBreadcrumbNumbering = function()
{
	var breadcrumbs = this.manager.helper.get('#breadcrumbs');
	if(!breadcrumbs) { return; }
	
	var items = this.manager.helper.get('li', breadcrumbs);
	for(var i=0; i<items.length; i++)
	{
		var span = items[i].firstChild.firstChild;
		span.innerHTML = (i + 1) + '. ' + span.innerHTML;
	}
};





//add form focus handlers that implement the :focus state in IE7
FormsHelper.prototype.addFormFocusHandlers = function()
{
	//get the collection of inputs, and add the collection of textareas
	var inputs = this.manager.helper.get('input').concat(this.manager.helper.get('textarea'));
	
	//don't proceed if we don't have any
	if(inputs.length == 0) { return; }
	
	//don't do this for IE6, because .focus class rules are unstable
	if(this.msie6) { return; }
	
	//copy a reference to this
	var self = this;
	
	//iterate through them and add focus/blur handlers
	//that add and remove the "focus" class
	for(var i=0; i<inputs.length; i++)
	{
		this.manager.helper.listen(inputs[i], 'focus', function(e)
		{
			var target = e.srcElement;
			target.className += ' focus';
		});

		this.manager.helper.listen(inputs[i], 'blur', function(e)
		{
			var target = e.srcElement;
			target = self.manager.helper.removeClass(target, ' focus');
		});
	}
};







//add form tooltip handlers, if applicable
FormsHelper.prototype.addFormTooltipHandlers = function()
{
	//don't do this for IE6, because it's fucked beyond reasonable repair
	if(this.msie6) { return; }
	
	//look for tooltip elements, and don't proceed if we don't have any
	var tooltips = this.manager.helper.get('label', document, { 'class' : 'tooltip' });
	if(tooltips.length == 0) { return; }
	
	//copy a reference to this
	var self = this;

	//iterate through the tooltips to process each one
	for(var i=0; i<tooltips.length; i++)
	{
		//get the input that this tooltip refers to (if there is one), and save circular referenes
		//ie, save a tooltip property of the input, and an input property of the tooltip
		//for internet explorer we have to use its property name not its attribute name
		//http://reference.sitepoint.com/javascript/Element/getAttribute
		var input = this.manager.helper.get('#' + tooltips[i].getAttribute((this.manager.msie && !this.manager.msie8) ? 'htmlFor' : 'for'));
		if(!input) { continue; }
		tooltips[i].input = input;
		input.tooltip = tooltips[i];
		
		//if this is ie < 8 we need to deal with its contextual z-ordering bugginess
		//which stops the correct tooltip appearing at the top of the stack
		//so save a reference to the parent group, and set a base z-index
		if(this.manager.msie && !this.manager.msie8)
		{
			var fieldgroup = tooltips[i].parentNode;
			while(!this.manager.helper.hasClass(fieldgroup, 'fieldgroup'))
			{
				fieldgroup = fieldgroup.parentNode;
			}
			fieldgroup.style.zIndex = 1000;
			tooltips[i].fieldgroup = fieldgroup;
		}
		
		//then bind a focus handler to add the hover and focus class name to its tooltip
		this.manager.helper.listen(input, 'focus', function(e)
		{
			//get the target node (the input)
			var target = e.target ? e.target : e.srcElement;
						
			//if we have a current tooltip and it isn't this one, reset it, and its z-index in ie < 8
			if(typeof self.currentTooltip != 'undefined' && self.currentTooltip != target.tooltip)
			{
				self.currentTooltip = self.manager.helper.removeClass(self.currentTooltip, '[ ](hover|focus)');
				if(self.msie && !self.manager.msie8)
				{
					self.currentTooltip.fieldgroup.style.zIndex = 1000;
				}
				delete self.currentTooltip;
			}
			
			//add the hover and focus class and save the current tooltip, if necessary
			//and also increase its z-index in ie < 8 
			if(!self.manager.helper.hasClass(' hover focus'))
			{
				self.currentTooltip = target.tooltip;
				if(self.manager.msie && !self.manager.msie8)
				{
					self.currentTooltip.fieldgroup.style.zIndex = 2000;
				}
				self.currentTooltip.className += ' hover focus';
				
				//but if the tooltip appears partially below the fold, scroll the page to bring it into view
				//[which can happen if you just tab through the fields without manually scrolling too
				// because in some browsers the auto-sroll that occurs is only enough to bring the field into view]
				var tooltipextent = (self.manager.helper.getTopPosition(self.currentTooltip) + self.currentTooltip.offsetHeight), 
					canvasextent = (self.manager.helper.getCanvasHeight() + self.manager.helper.getScrollingTop());

				if(tooltipextent > canvasextent)
				{
					window.scrollBy(0, (tooltipextent - canvasextent))
				}
			}
		});
		
		//and a blur handler to clear it
		this.manager.helper.listen(input, 'blur', function(e)
		{
			//if we have a current tooltip, reset it, and its z-index in ie < 8
			if(typeof self.currentTooltip != 'undefined')
			{
				self.currentTooltip = self.manager.helper.removeClass(self.currentTooltip, '[ ](hover|focus)');
				if(self.manager.msie && !self.manager.msie8)
				{
					self.currentTooltip.fieldgroup.style.zIndex = 1000;
				}
				delete self.currentTooltip;
			}
		});

	}

	//bind a document mousemove handler to implement the "hover" state
	this.manager.helper.listen(document, 'mousemove', function(e)
	{
		//get the target and a flag that says whether we found a tooltip target
		var found = false, target = e.target ? e.target : e.srcElement;
		
		//iterate through the tooltips to see if the mouse event is inside one of them
		for(var i=0; i<tooltips.length; i++)
		{
			//if a tooltip contains this event target
			if(self.manager.helper.contains(tooltips[i], target))
			{
				//we found a tooltip target
				found = true;
				
				//if it doesn't already have the hover class
				if(!self.manager.helper.hasClass(tooltips[i], 'hover'))
				{
					//if we have a current tooltip, reset it, and its z-index in ie < 8
					if(typeof self.currentTooltip != 'undefined')
					{
						self.currentTooltip = self.manager.helper.removeClass(self.currentTooltip, '[ ](hover|focus)');
						if(self.manager.msie && !self.manager.msie8)
						{
							self.currentTooltip.fieldgroup.style.zIndex = 1000;
						}
						delete self.currentTooltip;
					}

					//add the hover class and save the current tooltip
					//and also increase its z-index in ie < 8
					self.currentTooltip = tooltips[i];
					if(self.manager.msie && !self.manager.msie8)
					{
						self.currentTooltip.fieldgroup.style.zIndex = 2000;
					}
					self.currentTooltip.className += ' hover';
				}
				
				//and we're done
				break;
			}
		}
		
		//if the mouse isn't inside a tooltip
		if(!found)
		{
			//if we have a current tooltip but it doesn't have the focus class, reset it
			//we check the focus class to prevent casual mouse movement clearing a focused tooltip
			if(typeof self.currentTooltip != 'undefined' && !self.manager.helper.hasClass(self.currentTooltip, 'focus'))
			{
				self.currentTooltip = self.manager.helper.removeClass(self.currentTooltip, ' hover');
				delete self.currentTooltip;
			}
		}
	});
	
};







//add handlers to text input fields that handles the default values
//this doesn't apply to fields which are readonly
FormsHelper.prototype.defaultValueHandler = function()
{
	//get and iterate through collection of text fields
	var fields = this.manager.helper.get('input', document, {'type':'text'});
	for(var i=0; i<fields.length; i++)
	{
		//don't do this for readonly fields, which are used to display static information in forms
		if(fields[i].getAttribute('readonly')) { continue; }
		
		//apply a focus listener
		this.manager.helper.listen(fields[i], 'focus', function(e)
		{
			//get a target reference
			var field = e.target ? e.target : e.srcElement;
			
			//if the value is default, clear it
			if(field.value == field.defaultValue)
			{
				field.value = '';
			}
		});
		
		//apply a blur listener
		this.manager.helper.listen(fields[i], 'blur', function(e)
		{
			//get a target reference
			var field = e.target ? e.target : e.srcElement;
			
			//if the value is still empty, restore the default
			if(field.value == '')
			{
				field.value = field.defaultValue;
			}
		});
	}
	
	//look for the "ask a question" textarea
	var askarea = this.manager.helper.get('#ask-a-question');
	if(!askarea) { return; }
	
	//reference to this
	var self = this;
	
	//bind a focus listener that adds the cleared class
	//which removes its background image
	this.manager.helper.listen(askarea, 'focus', function(e)
	{
		//get a target reference
		var field = e.target ? e.target : e.srcElement;
		
		//add the cleared class, if it doesn't already have it
		if(!self.manager.helper.hasClass(field, 'cleared'))
		{
			field.className += ' cleared';
		}
	});
	
	//bind a blur listener to restore it
	//if nothing has been typed in the area
	this.manager.helper.listen(askarea, 'blur', function(e)
	{
		//get a target reference
		var field = e.target ? e.target : e.srcElement;
		
		//if the value is empty or equal to default, remove the cleared class
		if(field.value == '' || field.value == field.defaultValue)
		{
			field = self.manager.helper.removeClass(field, ' cleared');
		}
	});
};






//initialize the dynamic preview used in step 4 of the sell process
FormsHelper.prototype.initDynamicPreview = function()
{
	//look for the listing-upgrades fieldset, and don't continue if it's not there
	var fieldset = this.manager.helper.get('#listing-upgrades');
	if(!fieldset) { return; }
	
	//reference to this
	var self = this;
	
	//get a reference to both <tr> elements in the preview table
	var rows = this.manager.helper.get('tr', fieldset);
	
	//toggle preview class function
	function togglePreviewClass(value, add)
	{
		for(var i=0; i<rows.length; i++)
		{
			if(add) { rows[i].className += ' ' + value; }
			else { rows[i] = self.manager.helper.removeClass(rows[i], '[ ]?' + value); }
		}
	}

	//bind a click handler to the fieldset
	this.manager.helper.listen(fieldset, 'click', function(e)
	{
		//get the target
		var target = e.target ? e.target : e.srcElement;
		
		//while the target is strong, em or text node, convert to parent
		while(/(strong|em|#text)/i.test(target.nodeName))
		{
			target = target.parentNode;
		}
		
		//if the target is a label, convert to checkbox reference
		if(/label/i.test(target.nodeName))
		{
			target = self.manager.helper.get('#' + target.getAttribute('for'));
		}
	
		//if we don't have a target, or it's not a checkbox,
		//we have nothing more to do here
		if(!target || !target.getAttribute('type') || target.getAttribute('type') != 'checkbox')
		{
			return;
		}
		
		//now switch by checkbox ID
		//to add or remove the applicable class from the rows
		//according to whether this checkbox is checked
		switch(target.id)
		{
			//highlight listing
			case 'upgrade-highlight' : 
				togglePreviewClass('highlight', target.checked);
				break;
				
			//bold title
			case 'upgrade-bold' :
				togglePreviewClass('bold', target.checked);
				break;
			
			//border
			case 'upgrade-border' :
				togglePreviewClass('border', target.checked);
				break;
		}
	});
	
	//now if the upgrade highlight or upgrade bold checkboxes 
	//are already checked by default, add the preview classes
	togglePreviewClass('highlight', this.manager.helper.get('#upgrade-highlight').checked);
	togglePreviewClass('bold', this.manager.helper.get('#upgrade-bold').checked);
	
};







//add show password checkbox, if applicable
FormsHelper.prototype.addShowPassword = function()
{
	//look for the change password fieldset, and don't continue if it's not there
	var fieldset = this.manager.helper.get('#change-password');
	if(!fieldset) { return; }
	
	//dont do this in ie, in which it will fail, because we can't set input types in JS
	if(this.manager.msie) { return; }
	
	//save references to the password fields
	var fields = this.manager.helper.get('input', fieldset, {'type':'password'});
	
	//create the showpassword field
	var fieldrow = this.manager.helper.element('div', {'class':'fieldrow'});
	fieldrow.appendChild(this.manager.helper.element('label'));
	var fieldgroup = fieldrow.appendChild(this.manager.helper.element('span', {'class':'fieldgroup'}));
	var innerrow = fieldgroup.appendChild(this.manager.helper.element('span', {'class':'fieldrow'}));
	innerrow.appendChild(this.manager.helper.element('input', {
		'class' : 'checkbox',
		'type' : 'checkbox',
		'id' : 'show-password'
		}));
	innerrow.appendChild(this.manager.helper.element('label', {
		'for' : 'show-password',
		'#text' : this.manager.lang['show-password']['label']
		}));
	fieldset.appendChild(fieldrow);

	//re-tweak radio and checkbox positions
	this.tweakRadioAndCheckboxPositions();

	//reference to this
	var self = this;

	//bind a click handler to the inner row
	//to handle clicks on the checkbox and label
	this.manager.helper.listen(innerrow, 'click', function(e)
	{
		//target reference; no need for target forking because we don't support IE
		var target = e.target;
		
		//convert label reference
		if(/label/i.test(target.nodeName)) { target = target.previousSibling; }
		
		//set the input type of the password fields 
		//according to whether the checkbox is checked
		for(var i=0; i<fields.length; i++)
		{
			fields[i].setAttribute('type', target.checked ? 'text' : 'password');
		}
	});
};


// ****************************
// USER FILTERS
// ***************************

(function() {
  jQuery(document).ready(function() {
  
    var timer;
    
    jQuery("#customfilters").parent().bind("mouseover", function() {
      
      try {
        window.clearTimeout(timer);
      } catch (e) {}
      
      if (!jQuery(this).hasClass('generated'))
      {
        jQuery(this).addClass("generated").append(
          _createMenuFromOptions(flippaFilterOptions)
        );
      }
      else
      {
        jQuery("#filteroptions").fadeIn("fast");
      }
    });
  
    jQuery("#customfilters").parent().bind("mouseout", function() {
    
      timer = window.setTimeout(function() {
          jQuery("#filteroptions").fadeOut("fast");
        },
        200
      );
    
    });
  
  });
  
  var _createMenuFromOptions = function _createMenuFromOptions(options) {
    var ul = document.createElement("ul");
    ul.id = "filteroptions";
    
    var count = 0;
    for (var url in options) {
      ++count;
      var li = document.createElement("li");
      var a = document.createElement("a");
      var t = document.createTextNode(options[url]);
      a.href = url;
      a.appendChild(t);
      li.appendChild(a);
      ul.appendChild(li);
    }
    
    if (count > 0) {
      return ul;
    }
  };
  
})();


// SER1.0 :: Search Results 
//***************************************************************************
// (c) SitePoint Pty Ltd. -- http://www.sitepoint.com/ -- All Rights Reserved
//***************************************************************************


//search results class handles all scripting 
//used on the buy search results pages
//and on pages that have the saved searches menu
function SearchResults()
{

}



//initialization method
SearchResults.prototype.init = function()
{
	//bind handlers for the saved searches menu
	this.initSavedSearchesMenu();
};


//bind handlers for the saved searches menu
SearchResults.prototype.initSavedSearchesMenu = function()
{
	//get a reference to the trigger link
	var trigger = this.manager.helper.get('#saved-searches');
	if(!trigger) { return; }	//just in case!
	
	//reference to this
	var self = this;
	
	//bind a focus handler that activates the menu
	this.manager.helper.listen(trigger, 'focus', function(e)
	{
		//get the event target
		var target = e.target ? e.target : e.srcElement;
		
		//convert any text node reference for safari
		if(target.nodeName == '#text') { target = target.parentNode; }
		
		//get a reference to the menu
		var menu = self.manager.helper.get('div', target.parentNode);
		if(!menu) { return; }	//jsut in case!
		menu = menu[0];
		
		//set the visible class to make it visible
		menu.className += ' visible'
		
		//if this is IE6 we need to set the position differently to make it show up
		if(self.manager.msie6)
		{
			menu.style.left = '0';
		}
		
		//set the width to match its parent
		menu.style.width = (parseInt(target.parentNode.offsetWidth) + 20) + 'px';
		
		//and set the hover class on the list item to fix its rollover state
		target.parentNode.className += ' hover';
	});

	//bind a click handler that fires the focus handler
	//this saves redefining the functionality twice
	//actually clicking the link will focus it anyway
	//but do this anyway, because that's not 100% reliable
	this.manager.helper.listen(trigger, 'click', function(e)
	{
		//fire the focus handler
		trigger.focus();
				
		//don't follow the link
		try { e.preventDefault(); } catch(err) {}
		return false;
	});

	//bind a generic document mousedown to reset the menu
	this.manager.helper.listen(document, 'mousedown', function(e)
	{
		//get the event target
		var target = e.target ? e.target : e.srcElement;
		
		//if the target is not contained by the trigger, reset the menu
		if(!self.manager.helper.contains(trigger.parentNode, target))
		{
			//get the menu reference
			var menu = self.manager.helper.get('div', trigger.parentNode);
			if(!menu) { return; }	//jsut in case!
			menu = menu[0];
			
			//get the list item reference
			var listitem = trigger.parentNode;
		
			//if this is IE6 reset the position manually (as well as removing the class, obviously)
			if(self.manager.msie6)
			{
				menu = self.manager.helper.removeClass(menu, ' visible');
				menu.style.left = '-100em';

				//remove the hover class from the list item
				listitem = self.manager.helper.removeClass(listitem, ' hover');
			}
			
			//or if this is another version of IE, remove the class without fading out
			//there's no fadeout in IE7 because it would require applying an opacity filter to the menu
			//which already has a 24 bit PNG as its background, and a combination of the two in iE7
			//make the background come out completely black, which is obviously no good
			else if(self.manager.msie)
			{
				menu = self.manager.helper.removeClass(menu, ' visible');

				//remove the hover class from the list item
				listitem = self.manager.helper.removeClass(listitem, ' hover');
			}
			
			//otherwise do a fadeout and remove the class oncomplete
			else
			{
				self.manager.helper.fade(menu, 3, function()
				{
					menu = self.manager.helper.removeClass(menu, ' visible');
					setTimeout(function() { menu.style.opacity = 1; }, 100);

					//remove the hover class from the list item
					listitem = self.manager.helper.removeClass(listitem, ' hover');
				});
			}
		}
	});

	//bind a blur handler to every link, input and button in the content area
	//so we can detect tabbing away from the menu
	var menu = this.manager.helper.get('div', trigger.parentNode)[0],
		contentarea = this.manager.helper.get('#content');
	var focussables = this.manager.helper.get('a', contentarea)
		.concat(this.manager.helper.get('input', contentarea))
		.concat(this.manager.helper.get('button', contentarea));
	for(var i=0; i<focussables.length; i++)
	{
		this.manager.helper.listen(focussables[i], 'focus', function(e)
		{
			//get the event target
			var target = e.target ? e.target : e.srcElement;
			
			//if the target is inside the menu, but not the trigger,
			//and the menu is open, then close it
			if(!self.manager.helper.contains(menu, target) 
				&& target != trigger 
				&& self.manager.helper.hasClass(menu, 'visible'))
			{
				//remove the visible class from the menu
				//we're not doing the fade out here because i don't think its appropriate
				menu = self.manager.helper.removeClass(menu, ' visible');
				
				//if this is IE6 reset the position manually
				if(self.manager.msie6)
				{
					menu.style.left = '-100em';
				}
				
				//remove the hover class from the list item
				var listitem = trigger.parentNode;
				listitem = self.manager.helper.removeClass(listitem, ' hover');
			}
			
			//if the target is inside the menu, but the menu isn't open, open it
			//this will catch tabbing into the menu backwards
			if(self.manager.helper.contains(menu, target) 
				&& !self.manager.helper.hasClass(menu, 'visible'))
			{
				//set the visible class to make it visible
				menu.className += ' visible'
				
				//if this is IE6 we need to set the position differently to make it show up
				if(self.manager.msie6)
				{
					menu.style.left = '0';
				}
				
				//set the width to match its parent
				menu.style.width = trigger.parentNode.offsetWidth + 'px';
				
				//and set the hover class on the list item to fix its rollover state
				trigger.parentNode.className += ' hover';
			}
		});
	}
};



// THIS MUST BE LAST
//instantiate the site manager, passing the config and language objects
new SiteManager(config, lang);

