/*
Script: Core.js
	MooTools - My Object Oriented JavaScript Tools.

License:
	MIT-style license.

Copyright:
	Copyright (c) 2006-2008 [Valerio Proietti](http://mad4milk.net/).

Code & Documentation:
	[The MooTools production team](http://mootools.net/developers/).

Inspiration:
	- Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php)
	- Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php)
*/

var MooTools = {
	'version': '1.2.1',
	'build': '0d4845aab3d9a4fdee2f0d4a6dd59210e4b697cf'
};

var Native = function(options){
	options = options || {};
	var name = options.name;
	var legacy = options.legacy;
	var protect = options.protect;
	var methods = options.implement;
	var generics = options.generics;
	var initialize = options.initialize;
	var afterImplement = options.afterImplement || function(){};
	var object = initialize || legacy;
	generics = generics !== false;

	object.constructor = Native;
	object.$family = {name: 'native'};
	if (legacy && initialize) object.prototype = legacy.prototype;
	object.prototype.constructor = object;

	if (name){
		var family = name.toLowerCase();
		object.prototype.$family = {name: family};
		Native.typize(object, family);
	}

	var add = function(obj, name, method, force){
		if (!protect || force || !obj.prototype[name]) obj.prototype[name] = method;
		if (generics) Native.genericize(obj, name, protect);
		afterImplement.call(obj, name, method);
		return obj;
	};

	object.alias = function(a1, a2, a3){
		if (typeof a1 == 'string'){
			if ((a1 = this.prototype[a1])) return add(this, a2, a1, a3);
		}
		for (var a in a1) this.alias(a, a1[a], a2);
		return this;
	};

	object.implement = function(a1, a2, a3){
		if (typeof a1 == 'string') return add(this, a1, a2, a3);
		for (var p in a1) add(this, p, a1[p], a2);
		return this;
	};

	if (methods) object.implement(methods);

	return object;
};

Native.genericize = function(object, property, check){
	if ((!check || !object[property]) && typeof object.prototype[property] == 'function') object[property] = function(){
		var args = Array.prototype.slice.call(arguments);
		return object.prototype[property].apply(args.shift(), args);
	};
};

Native.implement = function(objects, properties){
	for (var i = 0, l = objects.length; i < l; i++) objects[i].implement(properties);
};

Native.typize = function(object, family){
	if (!object.type) object.type = function(item){
		return ($type(item) === family);
	};
};

(function(){
	var natives = {'Array': Array, 'Date': Date, 'Function': Function, 'Number': Number, 'RegExp': RegExp, 'String': String};
	for (var n in natives) new Native({name: n, initialize: natives[n], protect: true});

	var types = {'boolean': Boolean, 'native': Native, 'object': Object};
	for (var t in types) Native.typize(types[t], t);

	var generics = {
		'Array': ["concat", "indexOf", "join", "lastIndexOf", "pop", "push", "reverse", "shift", "slice", "sort", "splice", "toString", "unshift", "valueOf"],
		'String': ["charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "match", "replace", "search", "slice", "split", "substr", "substring", "toLowerCase", "toUpperCase", "valueOf"]
	};
	for (var g in generics){
		for (var i = generics[g].length; i--;) Native.genericize(window[g], generics[g][i], true);
	};
})();

var Hash = new Native({

	name: 'Hash',

	initialize: function(object){
		if ($type(object) == 'hash') object = $unlink(object.getClean());
		for (var key in object) this[key] = object[key];
		return this;
	}

});

Hash.implement({

	forEach: function(fn, bind){
		for (var key in this){
			if (this.hasOwnProperty(key)) fn.call(bind, this[key], key, this);
		}
	},

	getClean: function(){
		var clean = {};
		for (var key in this){
			if (this.hasOwnProperty(key)) clean[key] = this[key];
		}
		return clean;
	},

	getLength: function(){
		var length = 0;
		for (var key in this){
			if (this.hasOwnProperty(key)) length++;
		}
		return length;
	}

});

Hash.alias('forEach', 'each');

Array.implement({

	forEach: function(fn, bind){
		for (var i = 0, l = this.length; i < l; i++) fn.call(bind, this[i], i, this);
	}

});

Array.alias('forEach', 'each');

function $A(iterable){
	if (iterable.item){
		var array = [];
		for (var i = 0, l = iterable.length; i < l; i++) array[i] = iterable[i];
		return array;
	}
	return Array.prototype.slice.call(iterable);
};

function $arguments(i){
	return function(){
		return arguments[i];
	};
};

function $chk(obj){
	return !!(obj || obj === 0);
};

function $clear(timer){
	clearTimeout(timer);
	clearInterval(timer);
	return null;
};

function $defined(obj){
	return (obj != undefined);
};

function $each(iterable, fn, bind){
	var type = $type(iterable);
	((type == 'arguments' || type == 'collection' || type == 'array') ? Array : Hash).each(iterable, fn, bind);
};

function $empty(){};

function $extend(original, extended){
	for (var key in (extended || {})) original[key] = extended[key];
	return original;
};

function $H(object){
	return new Hash(object);
};

function $lambda(value){
	return (typeof value == 'function') ? value : function(){
		return value;
	};
};

function $merge(){
	var mix = {};
	for (var i = 0, l = arguments.length; i < l; i++){
		var object = arguments[i];
		if ($type(object) != 'object') continue;
		for (var key in object){
			var op = object[key], mp = mix[key];
			mix[key] = (mp && $type(op) == 'object' && $type(mp) == 'object') ? $merge(mp, op) : $unlink(op);
		}
	}
	return mix;
};

function $pick(){
	for (var i = 0, l = arguments.length; i < l; i++){
		if (arguments[i] != undefined) return arguments[i];
	}
	return null;
};

function $random(min, max){
	return Math.floor(Math.random() * (max - min + 1) + min);
};

function $splat(obj){
	var type = $type(obj);
	return (type) ? ((type != 'array' && type != 'arguments') ? [obj] : obj) : [];
};

var $time = Date.now || function(){
	return +new Date;
};

function $try(){
	for (var i = 0, l = arguments.length; i < l; i++){
		try {
			return arguments[i]();
		} catch(e){}
	}
	return null;
};

function $type(obj){
	if (obj == undefined) return false;
	if (obj.$family) return (obj.$family.name == 'number' && !isFinite(obj)) ? false : obj.$family.name;
	if (obj.nodeName){
		switch (obj.nodeType){
			case 1: return 'element';
			case 3: return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace';
		}
	} else if (typeof obj.length == 'number'){
		if (obj.callee) return 'arguments';
		else if (obj.item) return 'collection';
	}
	return typeof obj;
};

function $unlink(object){
	var unlinked;
	switch ($type(object)){
		case 'object':
			unlinked = {};
			for (var p in object) unlinked[p] = $unlink(object[p]);
		break;
		case 'hash':
			unlinked = new Hash(object);
		break;
		case 'array':
			unlinked = [];
			for (var i = 0, l = object.length; i < l; i++) unlinked[i] = $unlink(object[i]);
		break;
		default: return object;
	}
	return unlinked;
};


/*
Script: Browser.js
	The Browser Core. Contains Browser initialization, Window and Document, and the Browser Hash.

License:
	MIT-style license.
*/

var Browser = $merge({

	Engine: {name: 'unknown', version: 0},

	Platform: {name: (window.orientation != undefined) ? 'ipod' : (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase()},

	Features: {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)},

	Plugins: {},

	Engines: {

		presto: function(){
			return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925));
		},

		trident: function(){
			return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? 5 : 4);
		},

		webkit: function(){
			return (navigator.taintEnabled) ? false : ((Browser.Features.xpath) ? ((Browser.Features.query) ? 525 : 420) : 419);
		},

		gecko: function(){
			return (document.getBoxObjectFor == undefined) ? false : ((document.getElementsByClassName) ? 19 : 18);
		}

	}

}, Browser || {});

Browser.Platform[Browser.Platform.name] = true;

Browser.detect = function(){

	for (var engine in this.Engines){
		var version = this.Engines[engine]();
		if (version){
			this.Engine = {name: engine, version: version};
			this.Engine[engine] = this.Engine[engine + version] = true;
			break;
		}
	}

	return {name: engine, version: version};

};

Browser.detect();

Browser.Request = function(){
	return $try(function(){
		return new XMLHttpRequest();
	}, function(){
		return new ActiveXObject('MSXML2.XMLHTTP');
	});
};

Browser.Features.xhr = !!(Browser.Request());

Browser.Plugins.Flash = (function(){
	var version = ($try(function(){
		return navigator.plugins['Shockwave Flash'].description;
	}, function(){
		return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
	}) || '0 r0').match(/\d+/g);
	return {version: parseInt(version[0] || 0 + '.' + version[1] || 0), build: parseInt(version[2] || 0)};
})();

function $exec(text){
	if (!text) return text;
	if (window.execScript){
		window.execScript(text);
	} else {
		var script = document.createElement('script');
		script.setAttribute('type', 'text/javascript');
		script[(Browser.Engine.webkit && Browser.Engine.version < 420) ? 'innerText' : 'text'] = text;
		document.head.appendChild(script);
		document.head.removeChild(script);
	}
	return text;
};

Native.UID = 1;

var $uid = (Browser.Engine.trident) ? function(item){
	return (item.uid || (item.uid = [Native.UID++]))[0];
} : function(item){
	return item.uid || (item.uid = Native.UID++);
};

var Window = new Native({

	name: 'Window',

	legacy: (Browser.Engine.trident) ? null: window.Window,

	initialize: function(win){
		$uid(win);
		if (!win.Element){
			win.Element = $empty;
			if (Browser.Engine.webkit) win.document.createElement("iframe"); //fixes safari 2
			win.Element.prototype = (Browser.Engine.webkit) ? window["[[DOMElement.prototype]]"] : {};
		}
		win.document.window = win;
		return $extend(win, Window.Prototype);
	},

	afterImplement: function(property, value){
		window[property] = Window.Prototype[property] = value;
	}

});

Window.Prototype = {$family: {name: 'window'}};

new Window(window);

var Document = new Native({

	name: 'Document',

	legacy: (Browser.Engine.trident) ? null: window.Document,

	initialize: function(doc){
		$uid(doc);
		doc.head = doc.getElementsByTagName('head')[0];
		doc.html = doc.getElementsByTagName('html')[0];
		if (Browser.Engine.trident && Browser.Engine.version <= 4) $try(function(){
			doc.execCommand("BackgroundImageCache", false, true);
		});
		if (Browser.Engine.trident) doc.window.attachEvent('onunload', function() {
			doc.window.detachEvent('onunload', arguments.callee);
			doc.head = doc.html = doc.window = null;
		});
		return $extend(doc, Document.Prototype);
	},

	afterImplement: function(property, value){
		document[property] = Document.Prototype[property] = value;
	}

});

Document.Prototype = {$family: {name: 'document'}};

new Document(document);


/*
Script: Array.js
	Contains Array Prototypes like each, contains, and erase.

License:
	MIT-style license.
*/

Array.implement({

	every: function(fn, bind){
		for (var i = 0, l = this.length; i < l; i++){
			if (!fn.call(bind, this[i], i, this)) return false;
		}
		return true;
	},

	filter: function(fn, bind){
		var results = [];
		for (var i = 0, l = this.length; i < l; i++){
			if (fn.call(bind, this[i], i, this)) results.push(this[i]);
		}
		return results;
	},

	clean: function() {
		return this.filter($defined);
	},

	indexOf: function(item, from){
		var len = this.length;
		for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
			if (this[i] === item) return i;
		}
		return -1;
	},

	map: function(fn, bind){
		var results = [];
		for (var i = 0, l = this.length; i < l; i++) results[i] = fn.call(bind, this[i], i, this);
		return results;
	},

	some: function(fn, bind){
		for (var i = 0, l = this.length; i < l; i++){
			if (fn.call(bind, this[i], i, this)) return true;
		}
		return false;
	},

	associate: function(keys){
		var obj = {}, length = Math.min(this.length, keys.length);
		for (var i = 0; i < length; i++) obj[keys[i]] = this[i];
		return obj;
	},

	link: function(object){
		var result = {};
		for (var i = 0, l = this.length; i < l; i++){
			for (var key in object){
				if (object[key](this[i])){
					result[key] = this[i];
					delete object[key];
					break;
				}
			}
		}
		return result;
	},

	contains: function(item, from){
		return this.indexOf(item, from) != -1;
	},

	extend: function(array){
		for (var i = 0, j = array.length; i < j; i++) this.push(array[i]);
		return this;
	},

	getLast: function(){
		return (this.length) ? this[this.length - 1] : null;
	},

	getRandom: function(){
		return (this.length) ? this[$random(0, this.length - 1)] : null;
	},

	include: function(item){
		if (!this.contains(item)) this.push(item);
		return this;
	},

	combine: function(array){
		for (var i = 0, l = array.length; i < l; i++) this.include(array[i]);
		return this;
	},

	erase: function(item){
		for (var i = this.length; i--; i){
			if (this[i] === item) this.splice(i, 1);
		}
		return this;
	},

	empty: function(){
		this.length = 0;
		return this;
	},

	flatten: function(){
		var array = [];
		for (var i = 0, l = this.length; i < l; i++){
			var type = $type(this[i]);
			if (!type) continue;
			array = array.concat((type == 'array' || type == 'collection' || type == 'arguments') ? Array.flatten(this[i]) : this[i]);
		}
		return array;
	},

	hexToRgb: function(array){
		if (this.length != 3) return null;
		var rgb = this.map(function(value){
			if (value.length == 1) value += value;
			return value.toInt(16);
		});
		return (array) ? rgb : 'rgb(' + rgb + ')';
	},

	rgbToHex: function(array){
		if (this.length < 3) return null;
		if (this.length == 4 && this[3] == 0 && !array) return 'transparent';
		var hex = [];
		for (var i = 0; i < 3; i++){
			var bit = (this[i] - 0).toString(16);
			hex.push((bit.length == 1) ? '0' + bit : bit);
		}
		return (array) ? hex : '#' + hex.join('');
	}

});


/*
Script: Function.js
	Contains Function Prototypes like create, bind, pass, and delay.

License:
	MIT-style license.
*/

Function.implement({

	extend: function(properties){
		for (var property in properties) this[property] = properties[property];
		return this;
	},

	create: function(options){
		var self = this;
		options = options || {};
		return function(event){
			var args = options.arguments;
			args = (args != undefined) ? $splat(args) : Array.slice(arguments, (options.event) ? 1 : 0);
			if (options.event) args = [event || window.event].extend(args);
			var returns = function(){
				return self.apply(options.bind || null, args);
			};
			if (options.delay) return setTimeout(returns, options.delay);
			if (options.periodical) return setInterval(returns, options.periodical);
			if (options.attempt) return $try(returns);
			return returns();
		};
	},

	run: function(args, bind){
		return this.apply(bind, $splat(args));
	},

	pass: function(args, bind){
		return this.create({bind: bind, arguments: args});
	},

	bind: function(bind, args){
		return this.create({bind: bind, arguments: args});
	},

	bindWithEvent: function(bind, args){
		return this.create({bind: bind, arguments: args, event: true});
	},

	attempt: function(args, bind){
		return this.create({bind: bind, arguments: args, attempt: true})();
	},

	delay: function(delay, bind, args){
		return this.create({bind: bind, arguments: args, delay: delay})();
	},

	periodical: function(periodical, bind, args){
		return this.create({bind: bind, arguments: args, periodical: periodical})();
	}

});


/*
Script: Number.js
	Contains Number Prototypes like limit, round, times, and ceil.

License:
	MIT-style license.
*/

Number.implement({

	limit: function(min, max){
		return Math.min(max, Math.max(min, this));
	},

	round: function(precision){
		precision = Math.pow(10, precision || 0);
		return Math.round(this * precision) / precision;
	},

	times: function(fn, bind){
		for (var i = 0; i < this; i++) fn.call(bind, i, this);
	},

	toFloat: function(){
		return parseFloat(this);
	},

	toInt: function(base){
		return parseInt(this, base || 10);
	}

});

Number.alias('times', 'each');

(function(math){
	var methods = {};
	math.each(function(name){
		if (!Number[name]) methods[name] = function(){
			return Math[name].apply(null, [this].concat($A(arguments)));
		};
	});
	Number.implement(methods);
})(['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'sin', 'sqrt', 'tan']);


/*
Script: String.js
	Contains String Prototypes like camelCase, capitalize, test, and toInt.

License:
	MIT-style license.
*/

String.implement({

	test: function(regex, params){
		return ((typeof regex == 'string') ? new RegExp(regex, params) : regex).test(this);
	},

	contains: function(string, separator){
		return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1;
	},

	trim: function(){
		return this.replace(/^\s+|\s+$/g, '');
	},

	clean: function(){
		return this.replace(/\s+/g, ' ').trim();
	},

	camelCase: function(){
		return this.replace(/-\D/g, function(match){
			return match.charAt(1).toUpperCase();
		});
	},

	hyphenate: function(){
		return this.replace(/[A-Z]/g, function(match){
			return ('-' + match.charAt(0).toLowerCase());
		});
	},

	capitalize: function(){
		return this.replace(/\b[a-z]/g, function(match){
			return match.toUpperCase();
		});
	},

	escapeRegExp: function(){
		return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
	},

	toInt: function(base){
		return parseInt(this, base || 10);
	},

	toFloat: function(){
		return parseFloat(this);
	},

	hexToRgb: function(array){
		var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
		return (hex) ? hex.slice(1).hexToRgb(array) : null;
	},

	rgbToHex: function(array){
		var rgb = this.match(/\d{1,3}/g);
		return (rgb) ? rgb.rgbToHex(array) : null;
	},

	stripScripts: function(option){
		var scripts = '';
		var text = this.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(){
			scripts += arguments[1] + '\n';
			return '';
		});
		if (option === true) $exec(scripts);
		else if ($type(option) == 'function') option(scripts, text);
		return text;
	},

	substitute: function(object, regexp){
		return this.replace(regexp || (/\\?\{([^{}]+)\}/g), function(match, name){
			if (match.charAt(0) == '\\') return match.slice(1);
			return (object[name] != undefined) ? object[name] : '';
		});
	}

});


/*
Script: Hash.js
	Contains Hash Prototypes. Provides a means for overcoming the JavaScript practical impossibility of extending native Objects.

License:
	MIT-style license.
*/

Hash.implement({

	has: Object.prototype.hasOwnProperty,

	keyOf: function(value){
		for (var key in this){
			if (this.hasOwnProperty(key) && this[key] === value) return key;
		}
		return null;
	},

	hasValue: function(value){
		return (Hash.keyOf(this, value) !== null);
	},

	extend: function(properties){
		Hash.each(properties, function(value, key){
			Hash.set(this, key, value);
		}, this);
		return this;
	},

	combine: function(properties){
		Hash.each(properties, function(value, key){
			Hash.include(this, key, value);
		}, this);
		return this;
	},

	erase: function(key){
		if (this.hasOwnProperty(key)) delete this[key];
		return this;
	},

	get: function(key){
		return (this.hasOwnProperty(key)) ? this[key] : null;
	},

	set: function(key, value){
		if (!this[key] || this.hasOwnProperty(key)) this[key] = value;
		return this;
	},

	empty: function(){
		Hash.each(this, function(value, key){
			delete this[key];
		}, this);
		return this;
	},

	include: function(key, value){
		var k = this[key];
		if (k == undefined) this[key] = value;
		return this;
	},

	map: function(fn, bind){
		var results = new Hash;
		Hash.each(this, function(value, key){
			results.set(key, fn.call(bind, value, key, this));
		}, this);
		return results;
	},

	filter: function(fn, bind){
		var results = new Hash;
		Hash.each(this, function(value, key){
			if (fn.call(bind, value, key, this)) results.set(key, value);
		}, this);
		return results;
	},

	every: function(fn, bind){
		for (var key in this){
			if (this.hasOwnProperty(key) && !fn.call(bind, this[key], key)) return false;
		}
		return true;
	},

	some: function(fn, bind){
		for (var key in this){
			if (this.hasOwnProperty(key) && fn.call(bind, this[key], key)) return true;
		}
		return false;
	},

	getKeys: function(){
		var keys = [];
		Hash.each(this, function(value, key){
			keys.push(key);
		});
		return keys;
	},

	getValues: function(){
		var values = [];
		Hash.each(this, function(value){
			values.push(value);
		});
		return values;
	},

	toQueryString: function(base){
		var queryString = [];
		Hash.each(this, function(value, key){
			if (base) key = base + '[' + key + ']';
			var result;
			switch ($type(value)){
				case 'object': result = Hash.toQueryString(value, key); break;
				case 'array':
					var qs = {};
					value.each(function(val, i){
						qs[i] = val;
					});
					result = Hash.toQueryString(qs, key);
				break;
				default: result = key + '=' + encodeURIComponent(value);
			}
			if (value != undefined) queryString.push(result);
		});

		return queryString.join('&');
	}

});

Hash.alias({keyOf: 'indexOf', hasValue: 'contains'});


/*
Script: Event.js
	Contains the Event Native, to make the event object completely crossbrowser.

License:
	MIT-style license.
*/

var Event = new Native({

	name: 'Event',

	initialize: function(event, win){
		win = win || window;
		var doc = win.document;
		event = event || win.event;
		if (event.$extended) return event;
		this.$extended = true;
		var type = event.type;
		var target = event.target || event.srcElement;
		while (target && target.nodeType == 3) target = target.parentNode;

		if (type.test(/key/)){
			var code = event.which || event.keyCode;
			var key = Event.Keys.keyOf(code);
			if (type == 'keydown'){
				var fKey = code - 111;
				if (fKey > 0 && fKey < 13) key = 'f' + fKey;
			}
			key = key || String.fromCharCode(code).toLowerCase();
		} else if (type.match(/(click|mouse|menu)/i)){
			doc = (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
			var page = {
				x: event.pageX || event.clientX + doc.scrollLeft,
				y: event.pageY || event.clientY + doc.scrollTop
			};
			var client = {
				x: (event.pageX) ? event.pageX - win.pageXOffset : event.clientX,
				y: (event.pageY) ? event.pageY - win.pageYOffset : event.clientY
			};
			if (type.match(/DOMMouseScroll|mousewheel/)){
				var wheel = (event.wheelDelta) ? event.wheelDelta / 120 : -(event.detail || 0) / 3;
			}
			var rightClick = (event.which == 3) || (event.button == 2);
			var related = null;
			if (type.match(/over|out/)){
				switch (type){
					case 'mouseover': related = event.relatedTarget || event.fromElement; break;
					case 'mouseout': related = event.relatedTarget || event.toElement;
				}
				if (!(function(){
					while (related && related.nodeType == 3) related = related.parentNode;
					return true;
				}).create({attempt: Browser.Engine.gecko})()) related = false;
			}
		}

		return $extend(this, {
			event: event,
			type: type,

			page: page,
			client: client,
			rightClick: rightClick,

			wheel: wheel,

			relatedTarget: related,
			target: target,

			code: code,
			key: key,

			shift: event.shiftKey,
			control: event.ctrlKey,
			alt: event.altKey,
			meta: event.metaKey
		});
	}

});

Event.Keys = new Hash({
	'enter': 13,
	'up': 38,
	'down': 40,
	'left': 37,
	'right': 39,
	'esc': 27,
	'space': 32,
	'backspace': 8,
	'tab': 9,
	'delete': 46
});

Event.implement({

	stop: function(){
		return this.stopPropagation().preventDefault();
	},

	stopPropagation: function(){
		if (this.event.stopPropagation) this.event.stopPropagation();
		else this.event.cancelBubble = true;
		return this;
	},

	preventDefault: function(){
		if (this.event.preventDefault) this.event.preventDefault();
		else this.event.returnValue = false;
		return this;
	}

});


/*
Script: Class.js
	Contains the Class Function for easily creating, extending, and implementing reusable Classes.

License:
	MIT-style license.
*/

var Class = new Native({

	name: 'Class',

	initialize: function(properties){
		properties = properties || {};
		var klass = function(){
			for (var key in this){
				if ($type(this[key]) != 'function') this[key] = $unlink(this[key]);
			}
			this.constructor = klass;
			if (Class.prototyping) return this;
			var instance = (this.initialize) ? this.initialize.apply(this, arguments) : this;
			if (this.options && this.options.initialize) this.options.initialize.call(this);
			return instance;
		};

		for (var mutator in Class.Mutators){
			if (!properties[mutator]) continue;
			properties = Class.Mutators[mutator](properties, properties[mutator]);
			delete properties[mutator];
		}

		$extend(klass, this);
		klass.constructor = Class;
		klass.prototype = properties;
		return klass;
	}

});

Class.Mutators = {

	Extends: function(self, klass){
		Class.prototyping = klass.prototype;
		var subclass = new klass;
		delete subclass.parent;
		subclass = Class.inherit(subclass, self);
		delete Class.prototyping;
		return subclass;
	},

	Implements: function(self, klasses){
		$splat(klasses).each(function(klass){
			Class.prototying = klass;
			$extend(self, ($type(klass) == 'class') ? new klass : klass);
			delete Class.prototyping;
		});
		return self;
	}

};

Class.extend({

	inherit: function(object, properties){
		var caller = arguments.callee.caller;
		for (var key in properties){
			var override = properties[key];
			var previous = object[key];
			var type = $type(override);
			if (previous && type == 'function'){
				if (override != previous){
					if (caller){
						override.__parent = previous;
						object[key] = override;
					} else {
						Class.override(object, key, override);
					}
				}
			} else if(type == 'object'){
				object[key] = $merge(previous, override);
			} else {
				object[key] = override;
			}
		}

		if (caller) object.parent = function(){
			return arguments.callee.caller.__parent.apply(this, arguments);
		};

		return object;
	},

	override: function(object, name, method){
		var parent = Class.prototyping;
		if (parent && object[name] != parent[name]) parent = null;
		var override = function(){
			var previous = this.parent;
			this.parent = parent ? parent[name] : object[name];
			var value = method.apply(this, arguments);
			this.parent = previous;
			return value;
		};
		object[name] = override;
	}

});

Class.implement({

	implement: function(){
		var proto = this.prototype;
		$each(arguments, function(properties){
			Class.inherit(proto, properties);
		});
		return this;
	}

});


/*
Script: Class.Extras.js
	Contains Utility Classes that can be implemented into your own Classes to ease the execution of many common tasks.

License:
	MIT-style license.
*/

var Chain = new Class({

	$chain: [],

	chain: function(){
		this.$chain.extend(Array.flatten(arguments));
		return this;
	},

	callChain: function(){
		return (this.$chain.length) ? this.$chain.shift().apply(this, arguments) : false;
	},

	clearChain: function(){
		this.$chain.empty();
		return this;
	}

});

var Events = new Class({

	$events: {},

	addEvent: function(type, fn, internal){
		type = Events.removeOn(type);
		if (fn != $empty){
			this.$events[type] = this.$events[type] || [];
			this.$events[type].include(fn);
			if (internal) fn.internal = true;
		}
		return this;
	},

	addEvents: function(events){
		for (var type in events) this.addEvent(type, events[type]);
		return this;
	},

	fireEvent: function(type, args, delay){
		type = Events.removeOn(type);
		if (!this.$events || !this.$events[type]) return this;
		this.$events[type].each(function(fn){
			fn.create({'bind': this, 'delay': delay, 'arguments': args})();
		}, this);
		return this;
	},

	removeEvent: function(type, fn){
		type = Events.removeOn(type);
		if (!this.$events[type]) return this;
		if (!fn.internal) this.$events[type].erase(fn);
		return this;
	},

	removeEvents: function(events){
		if ($type(events) == 'object'){
			for (var type in events) this.removeEvent(type, events[type]);
			return this;
		}
		if (events) events = Events.removeOn(events);
		for (var type in this.$events){
			if (events && events != type) continue;
			var fns = this.$events[type];
			for (var i = fns.length; i--; i) this.removeEvent(type, fns[i]);
		}
		return this;
	}

});

Events.removeOn = function(string){
	return string.replace(/^on([A-Z])/, function(full, first) {
		return first.toLowerCase();
	});
};

var Options = new Class({

	setOptions: function(){
		this.options = $merge.run([this.options].extend(arguments));
		if (!this.addEvent) return this;
		for (var option in this.options){
			if ($type(this.options[option]) != 'function' || !(/^on[A-Z]/).test(option)) continue;
			this.addEvent(option, this.options[option]);
			delete this.options[option];
		}
		return this;
	}

});


/*
Script: Element.js
	One of the most important items in MooTools. Contains the dollar function, the dollars function, and an handful of cross-browser,
	time-saver methods to let you easily work with HTML Elements.

License:
	MIT-style license.
*/

var Element = new Native({

	name: 'Element',

	legacy: window.Element,

	initialize: function(tag, props){
		var konstructor = Element.Constructors.get(tag);
		if (konstructor) return konstructor(props);
		if (typeof tag == 'string') return document.newElement(tag, props);
		return $(tag).set(props);
	},

	afterImplement: function(key, value){
		Element.Prototype[key] = value;
		if (Array[key]) return;
		Elements.implement(key, function(){
			var items = [], elements = true;
			for (var i = 0, j = this.length; i < j; i++){
				var returns = this[i][key].apply(this[i], arguments);
				items.push(returns);
				if (elements) elements = ($type(returns) == 'element');
			}
			return (elements) ? new Elements(items) : items;
		});
	}

});

Element.Prototype = {$family: {name: 'element'}};

Element.Constructors = new Hash;

var IFrame = new Native({

	name: 'IFrame',

	generics: false,

	initialize: function(){
		var params = Array.link(arguments, {properties: Object.type, iframe: $defined});
		var props = params.properties || {};
		var iframe = $(params.iframe) || false;
		var onload = props.onload || $empty;
		delete props.onload;
		props.id = props.name = $pick(props.id, props.name, iframe.id, iframe.name, 'IFrame_' + $time());
		iframe = new Element(iframe || 'iframe', props);
		var onFrameLoad = function(){
			var host = $try(function(){
				return iframe.contentWindow.location.host;
			});
			if (host && host == window.location.host){
				var win = new Window(iframe.contentWindow);
				new Document(iframe.contentWindow.document);
				$extend(win.Element.prototype, Element.Prototype);
			}
			onload.call(iframe.contentWindow, iframe.contentWindow.document);
		};
		(window.frames[props.id]) ? onFrameLoad() : iframe.addListener('load', onFrameLoad);
		return iframe;
	}

});

var Elements = new Native({

	initialize: function(elements, options){
		options = $extend({ddup: true, cash: true}, options);
		elements = elements || [];
		if (options.ddup || options.cash){
			var uniques = {}, returned = [];
			for (var i = 0, l = elements.length; i < l; i++){
				var el = $.element(elements[i], !options.cash);
				if (options.ddup){
					if (uniques[el.uid]) continue;
					uniques[el.uid] = true;
				}
				returned.push(el);
			}
			elements = returned;
		}
		return (options.cash) ? $extend(elements, this) : elements;
	}

});

Elements.implement({

	filter: function(filter, bind){
		if (!filter) return this;
		return new Elements(Array.filter(this, (typeof filter == 'string') ? function(item){
			return item.match(filter);
		} : filter, bind));
	}

});

Document.implement({

	newElement: function(tag, props){
		if (Browser.Engine.trident && props){
			['name', 'type', 'checked'].each(function(attribute){
				if (!props[attribute]) return;
				tag += ' ' + attribute + '="' + props[attribute] + '"';
				if (attribute != 'checked') delete props[attribute];
			});
			tag = '<' + tag + '>';
		}
		return $.element(this.createElement(tag)).set(props);
	},

	newTextNode: function(text){
		return this.createTextNode(text);
	},

	getDocument: function(){
		return this;
	},

	getWindow: function(){
		return this.window;
	}

});

Window.implement({

	$: function(el, nocash){
		if (el && el.$family && el.uid) return el;
		var type = $type(el);
		return ($[type]) ? $[type](el, nocash, this.document) : null;
	},

	$$: function(selector){
		if (arguments.length == 1 && typeof selector == 'string') return this.document.getElements(selector);
		var elements = [];
		var args = Array.flatten(arguments);
		for (var i = 0, l = args.length; i < l; i++){
			var item = args[i];
			switch ($type(item)){
				case 'element': elements.push(item); break;
				case 'string': elements.extend(this.document.getElements(item, true));
			}
		}
		return new Elements(elements);
	},

	getDocument: function(){
		return this.document;
	},

	getWindow: function(){
		return this;
	}

});

$.string = function(id, nocash, doc){
	id = doc.getElementById(id);
	return (id) ? $.element(id, nocash) : null;
};

$.element = function(el, nocash){
	$uid(el);
	if (!nocash && !el.$family && !(/^object|embed$/i).test(el.tagName)){
		var proto = Element.Prototype;
		for (var p in proto) el[p] = proto[p];
	};
	return el;
};

$.object = function(obj, nocash, doc){
	if (obj.toElement) return $.element(obj.toElement(doc), nocash);
	return null;
};

$.textnode = $.whitespace = $.window = $.document = $arguments(0);

Native.implement([Element, Document], {

	getElement: function(selector, nocash){
		return $(this.getElements(selector, true)[0] || null, nocash);
	},

	getElements: function(tags, nocash){
		tags = tags.split(',');
		var elements = [];
		var ddup = (tags.length > 1);
		tags.each(function(tag){
			var partial = this.getElementsByTagName(tag.trim());
			(ddup) ? elements.extend(partial) : elements = partial;
		}, this);
		return new Elements(elements, {ddup: ddup, cash: !nocash});
	}

});

(function(){

var collected = {}, storage = {};
var props = {input: 'checked', option: 'selected', textarea: (Browser.Engine.webkit && Browser.Engine.version < 420) ? 'innerHTML' : 'value'};

var get = function(uid){
	return (storage[uid] || (storage[uid] = {}));
};

var clean = function(item, retain){
	if (!item) return;
	var uid = item.uid;
	if (Browser.Engine.trident){
		if (item.clearAttributes){
			var clone = retain && item.cloneNode(false);
			item.clearAttributes();
			if (clone) item.mergeAttributes(clone);
		} else if (item.removeEvents){
			item.removeEvents();
		}
		if ((/object/i).test(item.tagName)){
			for (var p in item){
				if (typeof item[p] == 'function') item[p] = $empty;
			}
			Element.dispose(item);
		}
	}	
	if (!uid) return;
	collected[uid] = storage[uid] = null;
};

var purge = function(){
	Hash.each(collected, clean);
	if (Browser.Engine.trident) $A(document.getElementsByTagName('object')).each(clean);
	if (window.CollectGarbage) CollectGarbage();
	collected = storage = null;
};

var walk = function(element, walk, start, match, all, nocash){
	var el = element[start || walk];
	var elements = [];
	while (el){
		if (el.nodeType == 1 && (!match || Element.match(el, match))){
			if (!all) return $(el, nocash);
			elements.push(el);
		}
		el = el[walk];
	}
	return (all) ? new Elements(elements, {ddup: false, cash: !nocash}) : null;
};

var attributes = {
	'html': 'innerHTML',
	'class': 'className',
	'for': 'htmlFor',
	'text': (Browser.Engine.trident || (Browser.Engine.webkit && Browser.Engine.version < 420)) ? 'innerText' : 'textContent'
};
var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readonly', 'multiple', 'selected', 'noresize', 'defer'];
var camels = ['value', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly', 'rowSpan', 'tabIndex', 'useMap'];

Hash.extend(attributes, bools.associate(bools));
Hash.extend(attributes, camels.associate(camels.map(String.toLowerCase)));

var inserters = {

	before: function(context, element){
		if (element.parentNode) element.parentNode.insertBefore(context, element);
	},

	after: function(context, element){
		if (!element.parentNode) return;
		var next = element.nextSibling;
		(next) ? element.parentNode.insertBefore(context, next) : element.parentNode.appendChild(context);
	},

	bottom: function(context, element){
		element.appendChild(context);
	},

	top: function(context, element){
		var first = element.firstChild;
		(first) ? element.insertBefore(context, first) : element.appendChild(context);
	}

};

inserters.inside = inserters.bottom;

Hash.each(inserters, function(inserter, where){

	where = where.capitalize();

	Element.implement('inject' + where, function(el){
		inserter(this, $(el, true));
		return this;
	});

	Element.implement('grab' + where, function(el){
		inserter($(el, true), this);
		return this;
	});

});

Element.implement({

	set: function(prop, value){
		switch ($type(prop)){
			case 'object':
				for (var p in prop) this.set(p, prop[p]);
				break;
			case 'string':
				var property = Element.Properties.get(prop);
				(property && property.set) ? property.set.apply(this, Array.slice(arguments, 1)) : this.setProperty(prop, value);
		}
		return this;
	},

	get: function(prop){
		var property = Element.Properties.get(prop);
		return (property && property.get) ? property.get.apply(this, Array.slice(arguments, 1)) : this.getProperty(prop);
	},

	erase: function(prop){
		var property = Element.Properties.get(prop);
		(property && property.erase) ? property.erase.apply(this) : this.removeProperty(prop);
		return this;
	},

	setProperty: function(attribute, value){
		var key = attributes[attribute];
		if (value == undefined) return this.removeProperty(attribute);
		if (key && bools[attribute]) value = !!value;
		(key) ? this[key] = value : this.setAttribute(attribute, '' + value);
		return this;
	},

	setProperties: function(attributes){
		for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
		return this;
	},

	getProperty: function(attribute){
		var key = attributes[attribute];
		var value = (key) ? this[key] : this.getAttribute(attribute, 2);
		return (bools[attribute]) ? !!value : (key) ? value : value || null;
	},

	getProperties: function(){
		var args = $A(arguments);
		return args.map(this.getProperty, this).associate(args);
	},

	removeProperty: function(attribute){
		var key = attributes[attribute];
		(key) ? this[key] = (key && bools[attribute]) ? false : '' : this.removeAttribute(attribute);
		return this;
	},

	removeProperties: function(){
		Array.each(arguments, this.removeProperty, this);
		return this;
	},

	hasClass: function(className){
		return this.className.contains(className, ' ');
	},

	addClass: function(className){
		if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean();
		return this;
	},

	removeClass: function(className){
		this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1');
		return this;
	},

	toggleClass: function(className){
		return this.hasClass(className) ? this.removeClass(className) : this.addClass(className);
	},

	adopt: function(){
		Array.flatten(arguments).each(function(element){
			element = $(element, true);
			if (element) this.appendChild(element);
		}, this);
		return this;
	},

	appendText: function(text, where){
		return this.grab(this.getDocument().newTextNode(text), where);
	},

	grab: function(el, where){
		inserters[where || 'bottom']($(el, true), this);
		return this;
	},

	inject: function(el, where){
		inserters[where || 'bottom'](this, $(el, true));
		return this;
	},

	replaces: function(el){
		el = $(el, true);
		el.parentNode.replaceChild(this, el);
		return this;
	},

	wraps: function(el, where){
		el = $(el, true);
		return this.replaces(el).grab(el, where);
	},

	getPrevious: function(match, nocash){
		return walk(this, 'previousSibling', null, match, false, nocash);
	},

	getAllPrevious: function(match, nocash){
		return walk(this, 'previousSibling', null, match, true, nocash);
	},

	getNext: function(match, nocash){
		return walk(this, 'nextSibling', null, match, false, nocash);
	},

	getAllNext: function(match, nocash){
		return walk(this, 'nextSibling', null, match, true, nocash);
	},

	getFirst: function(match, nocash){
		return walk(this, 'nextSibling', 'firstChild', match, false, nocash);
	},

	getLast: function(match, nocash){
		return walk(this, 'previousSibling', 'lastChild', match, false, nocash);
	},

	getParent: function(match, nocash){
		return walk(this, 'parentNode', null, match, false, nocash);
	},

	getParents: function(match, nocash){
		return walk(this, 'parentNode', null, match, true, nocash);
	},

	getChildren: function(match, nocash){
		return walk(this, 'nextSibling', 'firstChild', match, true, nocash);
	},

	getWindow: function(){
		return this.ownerDocument.window;
	},

	getDocument: function(){
		return this.ownerDocument;
	},

	getElementById: function(id, nocash){
		var el = this.ownerDocument.getElementById(id);
		if (!el) return null;
		for (var parent = el.parentNode; parent != this; parent = parent.parentNode){
			if (!parent) return null;
		}
		return $.element(el, nocash);
	},

	getSelected: function(){
		return new Elements($A(this.options).filter(function(option){
			return option.selected;
		}));
	},

	getComputedStyle: function(property){
		if (this.currentStyle) return this.currentStyle[property.camelCase()];
		var computed = this.getDocument().defaultView.getComputedStyle(this, null);
		return (computed) ? computed.getPropertyValue([property.hyphenate()]) : null;
	},

	toQueryString: function(){
		var queryString = [];
		this.getElements('input, select, textarea', true).each(function(el){
			if (!el.name || el.disabled) return;
			var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
				return opt.value;
			}) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;
			$splat(value).each(function(val){
				if (typeof val != 'undefined') queryString.push(el.name + '=' + encodeURIComponent(val));
			});
		});
		return queryString.join('&');
	},

	clone: function(contents, keepid){
		contents = contents !== false;
		var clone = this.cloneNode(contents);
		var clean = function(node, element){
			if (!keepid) node.removeAttribute('id');
			if (Browser.Engine.trident){
				node.clearAttributes();
				node.mergeAttributes(element);
				node.removeAttribute('uid');
				if (node.options){
					var no = node.options, eo = element.options;
					for (var j = no.length; j--;) no[j].selected = eo[j].selected;
				}
			}
			var prop = props[element.tagName.toLowerCase()];
			if (prop && element[prop]) node[prop] = element[prop];
		};

		if (contents){
			var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
			for (var i = ce.length; i--;) clean(ce[i], te[i]);
		}

		clean(clone, this);
		return $(clone);
	},

	destroy: function(){
		Element.empty(this);
		Element.dispose(this);
		clean(this, true);
		return null;
	},

	empty: function(){
		$A(this.childNodes).each(function(node){
			Element.destroy(node);
		});
		return this;
	},

	dispose: function(){
		return (this.parentNode) ? this.parentNode.removeChild(this) : this;
	},

	hasChild: function(el){
		el = $(el, true);
		if (!el) return false;
		if (Browser.Engine.webkit && Browser.Engine.version < 420) return $A(this.getElementsByTagName(el.tagName)).contains(el);
		return (this.contains) ? (this != el && this.contains(el)) : !!(this.compareDocumentPosition(el) & 16);
	},

	match: function(tag){
		return (!tag || (tag == this) || (Element.get(this, 'tag') == tag));
	}

});

Native.implement([Element, Window, Document], {

	addListener: function(type, fn){
		if (type == 'unload'){
			var old = fn, self = this;
			fn = function(){
				self.removeListener('unload', fn);
				old();
			};
		} else {
			collected[this.uid] = this;
		}
		if (this.addEventListener) this.addEventListener(type, fn, false);
		else this.attachEvent('on' + type, fn);
		return this;
	},

	removeListener: function(type, fn){
		if (this.removeEventListener) this.removeEventListener(type, fn, false);
		else this.detachEvent('on' + type, fn);
		return this;
	},

	retrieve: function(property, dflt){
		var storage = get(this.uid), prop = storage[property];
		if (dflt != undefined && prop == undefined) prop = storage[property] = dflt;
		return $pick(prop);
	},

	store: function(property, value){
		var storage = get(this.uid);
		storage[property] = value;
		return this;
	},

	eliminate: function(property){
		var storage = get(this.uid);
		delete storage[property];
		return this;
	}

});

window.addListener('unload', purge);

})();

Element.Properties = new Hash;

Element.Properties.style = {

	set: function(style){
		this.style.cssText = style;
	},

	get: function(){
		return this.style.cssText;
	},

	erase: function(){
		this.style.cssText = '';
	}

};

Element.Properties.tag = {

	get: function(){
		return this.tagName.toLowerCase();
	}

};

Element.Properties.html = (function(){
	var wrapper = document.createElement('div');

	var translations = {
		table: [1, '<table>', '</table>'],
		select: [1, '<select>', '</select>'],
		tbody: [2, '<table><tbody>', '</tbody></table>'],
		tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
	};
	translations.thead = translations.tfoot = translations.tbody;

	var html = {
		set: function(){
			var html = Array.flatten(arguments).join('');
			var wrap = Browser.Engine.trident && translations[this.get('tag')];
			if (wrap){
				var first = wrapper;
				first.innerHTML = wrap[1] + html + wrap[2];
				for (var i = wrap[0]; i--;) first = first.firstChild;
				this.empty().adopt(first.childNodes);
			} else {
				this.innerHTML = html;
			}
		}
	};

	html.erase = html.set;

	return html;
})();

if (Browser.Engine.webkit && Browser.Engine.version < 420) Element.Properties.text = {
	get: function(){
		if (this.innerText) return this.innerText;
		var temp = this.ownerDocument.newElement('div', {html: this.innerHTML}).inject(this.ownerDocument.body);
		var text = temp.innerText;
		temp.destroy();
		return text;
	}
};


/*
Script: Element.Event.js
	Contains Element methods for dealing with events, and custom Events.

License:
	MIT-style license.
*/

Element.Properties.events = {set: function(events){
	this.addEvents(events);
}};

Native.implement([Element, Window, Document], {

	addEvent: function(type, fn){
		var events = this.retrieve('events', {});
		events[type] = events[type] || {'keys': [], 'values': []};
		if (events[type].keys.contains(fn)) return this;
		events[type].keys.push(fn);
		var realType = type, custom = Element.Events.get(type), condition = fn, self = this;
		if (custom){
			if (custom.onAdd) custom.onAdd.call(this, fn);
			if (custom.condition){
				condition = function(event){
					if (custom.condition.call(this, event)) return fn.call(this, event);
					return true;
				};
			}
			realType = custom.base || realType;
		}
		var defn = function(){
			return fn.call(self);
		};
		var nativeEvent = Element.NativeEvents[realType];
		if (nativeEvent){
			if (nativeEvent == 2){
				defn = function(event){
					event = new Event(event, self.getWindow());
					if (condition.call(self, event) === false) event.stop();
				};
			}
			this.addListener(realType, defn);
		}
		events[type].values.push(defn);
		return this;
	},

	removeEvent: function(type, fn){
		var events = this.retrieve('events');
		if (!events || !events[type]) return this;
		var pos = events[type].keys.indexOf(fn);
		if (pos == -1) return this;
		events[type].keys.splice(pos, 1);
		var value = events[type].values.splice(pos, 1)[0];
		var custom = Element.Events.get(type);
		if (custom){
			if (custom.onRemove) custom.onRemove.call(this, fn);
			type = custom.base || type;
		}
		return (Element.NativeEvents[type]) ? this.removeListener(type, value) : this;
	},

	addEvents: function(events){
		for (var event in events) this.addEvent(event, events[event]);
		return this;
	},

	removeEvents: function(events){
		if ($type(events) == 'object'){
			for (var type in events) this.removeEvent(type, events[type]);
			return this;
		}
		var attached = this.retrieve('events');
		if (!attached) return this;
		if (!events){
			for (var type in attached) this.removeEvents(type);
			this.eliminate('events');
		} else if (attached[events]){
			while (attached[events].keys[0]) this.removeEvent(events, attached[events].keys[0]);
			attached[events] = null;
		}
		return this;
	},

	fireEvent: function(type, args, delay){
		var events = this.retrieve('events');
		if (!events || !events[type]) return this;
		events[type].keys.each(function(fn){
			fn.create({'bind': this, 'delay': delay, 'arguments': args})();
		}, this);
		return this;
	},

	cloneEvents: function(from, type){
		from = $(from);
		var fevents = from.retrieve('events');
		if (!fevents) return this;
		if (!type){
			for (var evType in fevents) this.cloneEvents(from, evType);
		} else if (fevents[type]){
			fevents[type].keys.each(function(fn){
				this.addEvent(type, fn);
			}, this);
		}
		return this;
	}

});

Element.NativeEvents = {
	click: 2, dblclick: 2, mouseup: 2, mousedown: 2, contextmenu: 2, //mouse buttons
	mousewheel: 2, DOMMouseScroll: 2, //mouse wheel
	mouseover: 2, mouseout: 2, mousemove: 2, selectstart: 2, selectend: 2, //mouse movement
	keydown: 2, keypress: 2, keyup: 2, //keyboard
	focus: 2, blur: 2, change: 2, reset: 2, select: 2, submit: 2, //form elements
	load: 1, unload: 1, beforeunload: 2, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window
	error: 1, abort: 1, scroll: 1 //misc
};

(function(){

var $check = function(event){
	var related = event.relatedTarget;
	if (related == undefined) return true;
	if (related === false) return false;
	return ($type(this) != 'document' && related != this && related.prefix != 'xul' && !this.hasChild(related));
};

Element.Events = new Hash({

	mouseenter: {
		base: 'mouseover',
		condition: $check
	},

	mouseleave: {
		base: 'mouseout',
		condition: $check
	},

	mousewheel: {
		base: (Browser.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel'
	}

});

})();


/*
Script: Element.Style.js
	Contains methods for interacting with the styles of Elements in a fashionable way.

License:
	MIT-style license.
*/

Element.Properties.styles = {set: function(styles){
	this.setStyles(styles);
}};

Element.Properties.opacity = {

	set: function(opacity, novisibility){
		if (!novisibility){
			if (opacity == 0){
				if (this.style.visibility != 'hidden') this.style.visibility = 'hidden';
			} else {
				if (this.style.visibility != 'visible') this.style.visibility = 'visible';
			}
		}
		if (!this.currentStyle || !this.currentStyle.hasLayout) this.style.zoom = 1;
		if (Browser.Engine.trident) this.style.filter = (opacity == 1) ? '' : 'alpha(opacity=' + opacity * 100 + ')';
		this.style.opacity = opacity;
		this.store('opacity', opacity);
	},

	get: function(){
		return this.retrieve('opacity', 1);
	}

};

Element.implement({

	setOpacity: function(value){
		return this.set('opacity', value, true);
	},

	getOpacity: function(){
		return this.get('opacity');
	},

	setStyle: function(property, value){
		switch (property){
			case 'opacity': return this.set('opacity', parseFloat(value));
			case 'float': property = (Browser.Engine.trident) ? 'styleFloat' : 'cssFloat';
		}
		property = property.camelCase();
		if ($type(value) != 'string'){
			var map = (Element.Styles.get(property) || '@').split(' ');
			value = $splat(value).map(function(val, i){
				if (!map[i]) return '';
				return ($type(val) == 'number') ? map[i].replace('@', Math.round(val)) : val;
			}).join(' ');
		} else if (value == String(Number(value))){
			value = Math.round(value);
		}
		this.style[property] = value;
		return this;
	},

	getStyle: function(property){
		switch (property){
			case 'opacity': return this.get('opacity');
			case 'float': property = (Browser.Engine.trident) ? 'styleFloat' : 'cssFloat';
		}
		property = property.camelCase();
		var result = this.style[property];
		if (!$chk(result)){
			result = [];
			for (var style in Element.ShortStyles){
				if (property != style) continue;
				for (var s in Element.ShortStyles[style]) result.push(this.getStyle(s));
				return result.join(' ');
			}
			result = this.getComputedStyle(property);
		}
		if (result){
			result = String(result);
			var color = result.match(/rgba?\([\d\s,]+\)/);
			if (color) result = result.replace(color[0], color[0].rgbToHex());
		}
		if (Browser.Engine.presto || (Browser.Engine.trident && !$chk(parseInt(result)))){
			if (property.test(/^(height|width)$/)){
				var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0;
				values.each(function(value){
					size += this.getStyle('border-' + value + '-width').toInt() + this.getStyle('padding-' + value).toInt();
				}, this);
				return this['offset' + property.capitalize()] - size + 'px';
			}
			if ((Browser.Engine.presto) && String(result).test('px')) return result;
			if (property.test(/(border(.+)Width|margin|padding)/)) return '0px';
		}
		return result;
	},

	setStyles: function(styles){
		for (var style in styles) this.setStyle(style, styles[style]);
		return this;
	},

	getStyles: function(){
		var result = {};
		Array.each(arguments, function(key){
			result[key] = this.getStyle(key);
		}, this);
		return result;
	}

});

Element.Styles = new Hash({
	left: '@px', top: '@px', bottom: '@px', right: '@px',
	width: '@px', height: '@px', maxWidth: '@px', maxHeight: '@px', minWidth: '@px', minHeight: '@px',
	backgroundColor: 'rgb(@, @, @)', backgroundPosition: '@px @px', color: 'rgb(@, @, @)',
	fontSize: '@px', letterSpacing: '@px', lineHeight: '@px', clip: 'rect(@px @px @px @px)',
	margin: '@px @px @px @px', padding: '@px @px @px @px', border: '@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)',
	borderWidth: '@px @px @px @px', borderStyle: '@ @ @ @', borderColor: 'rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)',
	zIndex: '@', 'zoom': '@', fontWeight: '@', textIndent: '@px', opacity: '@'
});

Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, borderStyle: {}, borderColor: {}};

['Top', 'Right', 'Bottom', 'Left'].each(function(direction){
	var Short = Element.ShortStyles;
	var All = Element.Styles;
	['margin', 'padding'].each(function(style){
		var sd = style + direction;
		Short[style][sd] = All[sd] = '@px';
	});
	var bd = 'border' + direction;
	Short.border[bd] = All[bd] = '@px @ rgb(@, @, @)';
	var bdw = bd + 'Width', bds = bd + 'Style', bdc = bd + 'Color';
	Short[bd] = {};
	Short.borderWidth[bdw] = Short[bd][bdw] = All[bdw] = '@px';
	Short.borderStyle[bds] = Short[bd][bds] = All[bds] = '@';
	Short.borderColor[bdc] = Short[bd][bdc] = All[bdc] = 'rgb(@, @, @)';
});


/*
Script: Element.Dimensions.js
	Contains methods to work with size, scroll, or positioning of Elements and the window object.

License:
	MIT-style license.

Credits:
	- Element positioning based on the [qooxdoo](http://qooxdoo.org/) code and smart browser fixes, [LGPL License](http://www.gnu.org/licenses/lgpl.html).
	- Viewport dimensions based on [YUI](http://developer.yahoo.com/yui/) code, [BSD License](http://developer.yahoo.com/yui/license.html).
*/

(function(){

Element.implement({

	scrollTo: function(x, y){
		if (isBody(this)){
			this.getWindow().scrollTo(x, y);
		} else {
			this.scrollLeft = x;
			this.scrollTop = y;
		}
		return this;
	},

	getSize: function(){
		if (isBody(this)) return this.getWindow().getSize();
		return {x: this.offsetWidth, y: this.offsetHeight};
	},

	getScrollSize: function(){
		if (isBody(this)) return this.getWindow().getScrollSize();
		return {x: this.scrollWidth, y: this.scrollHeight};
	},

	getScroll: function(){
		if (isBody(this)) return this.getWindow().getScroll();
		return {x: this.scrollLeft, y: this.scrollTop};
	},

	getScrolls: function(){
		var element = this, position = {x: 0, y: 0};
		while (element && !isBody(element)){
			position.x += element.scrollLeft;
			position.y += element.scrollTop;
			element = element.parentNode;
		}
		return position;
	},

	getOffsetParent: function(){
		var element = this;
		if (isBody(element)) return null;
		if (!Browser.Engine.trident) return element.offsetParent;
		while ((element = element.parentNode) && !isBody(element)){
			if (styleString(element, 'position') != 'static') return element;
		}
		return null;
	},

	getOffsets: function(){
		if (Browser.Engine.trident){
			var bound = this.getBoundingClientRect(), html = this.getDocument().documentElement;
			return {
				x: bound.left + html.scrollLeft - html.clientLeft,
				y: bound.top + html.scrollTop - html.clientTop
			};
		}

		var element = this, position = {x: 0, y: 0};
		if (isBody(this)) return position;

		while (element && !isBody(element)){
			position.x += element.offsetLeft;
			position.y += element.offsetTop;

			if (Browser.Engine.gecko){
				if (!borderBox(element)){
					position.x += leftBorder(element);
					position.y += topBorder(element);
				}
				var parent = element.parentNode;
				if (parent && styleString(parent, 'overflow') != 'visible'){
					position.x += leftBorder(parent);
					position.y += topBorder(parent);
				}
			} else if (element != this && Browser.Engine.webkit){
				position.x += leftBorder(element);
				position.y += topBorder(element);
			}

			element = element.offsetParent;
		}
		if (Browser.Engine.gecko && !borderBox(this)){
			position.x -= leftBorder(this);
			position.y -= topBorder(this);
		}
		return position;
	},

	getPosition: function(relative){
		if (isBody(this)) return {x: 0, y: 0};
		var offset = this.getOffsets(), scroll = this.getScrolls();
		var position = {x: offset.x - scroll.x, y: offset.y - scroll.y};
		var relativePosition = (relative && (relative = $(relative))) ? relative.getPosition() : {x: 0, y: 0};
		return {x: position.x - relativePosition.x, y: position.y - relativePosition.y};
	},

	getCoordinates: function(element){
		if (isBody(this)) return this.getWindow().getCoordinates();
		var position = this.getPosition(element), size = this.getSize();
		var obj = {left: position.x, top: position.y, width: size.x, height: size.y};
		obj.right = obj.left + obj.width;
		obj.bottom = obj.top + obj.height;
		return obj;
	},

	computePosition: function(obj){
		return {left: obj.x - styleNumber(this, 'margin-left'), top: obj.y - styleNumber(this, 'margin-top')};
	},

	position: function(obj){
		return this.setStyles(this.computePosition(obj));
	}

});

Native.implement([Document, Window], {

	getSize: function(){
		var win = this.getWindow();
		if (Browser.Engine.presto || Browser.Engine.webkit) return {x: win.innerWidth, y: win.innerHeight};
		var doc = getCompatElement(this);
		return {x: doc.clientWidth, y: doc.clientHeight};
	},

	getScroll: function(){
		var win = this.getWindow();
		var doc = getCompatElement(this);
		return {x: win.pageXOffset || doc.scrollLeft, y: win.pageYOffset || doc.scrollTop};
	},

	getScrollSize: function(){
		var doc = getCompatElement(this);
		var min = this.getSize();
		return {x: Math.max(doc.scrollWidth, min.x), y: Math.max(doc.scrollHeight, min.y)};
	},

	getPosition: function(){
		return {x: 0, y: 0};
	},

	getCoordinates: function(){
		var size = this.getSize();
		return {top: 0, left: 0, bottom: size.y, right: size.x, height: size.y, width: size.x};
	}

});

// private methods

var styleString = Element.getComputedStyle;

function styleNumber(element, style){
	return styleString(element, style).toInt() || 0;
};

function borderBox(element){
	return styleString(element, '-moz-box-sizing') == 'border-box';
};

function topBorder(element){
	return styleNumber(element, 'border-top-width');
};

function leftBorder(element){
	return styleNumber(element, 'border-left-width');
};

function isBody(element){
	return (/^(?:body|html)$/i).test(element.tagName);
};

function getCompatElement(element){
	var doc = element.getDocument();
	return (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
};

})();

//aliases

Native.implement([Window, Document, Element], {

	getHeight: function(){
		return this.getSize().y;
	},

	getWidth: function(){
		return this.getSize().x;
	},

	getScrollTop: function(){
		return this.getScroll().y;
	},

	getScrollLeft: function(){
		return this.getScroll().x;
	},

	getScrollHeight: function(){
		return this.getScrollSize().y;
	},

	getScrollWidth: function(){
		return this.getScrollSize().x;
	},

	getTop: function(){
		return this.getPosition().y;
	},

	getLeft: function(){
		return this.getPosition().x;
	}

});


/*
Script: Selectors.js
	Adds advanced CSS Querying capabilities for targeting elements. Also includes pseudoselectors support.

License:
	MIT-style license.
*/

Native.implement([Document, Element], {

	getElements: function(expression, nocash){
		expression = expression.split(',');
		var items, local = {};
		for (var i = 0, l = expression.length; i < l; i++){
			var selector = expression[i], elements = Selectors.Utils.search(this, selector, local);
			if (i != 0 && elements.item) elements = $A(elements);
			items = (i == 0) ? elements : (items.item) ? $A(items).concat(elements) : items.concat(elements);
		}
		return new Elements(items, {ddup: (expression.length > 1), cash: !nocash});
	}

});

Element.implement({

	match: function(selector){
		if (!selector || (selector == this)) return true;
		var tagid = Selectors.Utils.parseTagAndID(selector);
		var tag = tagid[0], id = tagid[1];
		if (!Selectors.Filters.byID(this, id) || !Selectors.Filters.byTag(this, tag)) return false;
		var parsed = Selectors.Utils.parseSelector(selector);
		return (parsed) ? Selectors.Utils.filter(this, parsed, {}) : true;
	}

});

var Selectors = {Cache: {nth: {}, parsed: {}}};

Selectors.RegExps = {
	id: (/#([\w-]+)/),
	tag: (/^(\w+|\*)/),
	quick: (/^(\w+|\*)$/),
	splitter: (/\s*([+>~\s])\s*([a-zA-Z#.*:\[])/g),
	combined: (/\.([\w-]+)|\[(\w+)(?:([!*^$~|]?=)(["']?)([^\4]*?)\4)?\]|:([\w-]+)(?:\(["']?(.*?)?["']?\)|$)/g)
};

Selectors.Utils = {

	chk: function(item, uniques){
		if (!uniques) return true;
		var uid = $uid(item);
		if (!uniques[uid]) return uniques[uid] = true;
		return false;
	},

	parseNthArgument: function(argument){
		if (Selectors.Cache.nth[argument]) return Selectors.Cache.nth[argument];
		var parsed = argument.match(/^([+-]?\d*)?([a-z]+)?([+-]?\d*)?$/);
		if (!parsed) return false;
		var inta = parseInt(parsed[1]);
		var a = (inta || inta === 0) ? inta : 1;
		var special = parsed[2] || false;
		var b = parseInt(parsed[3]) || 0;
		if (a != 0){
			b--;
			while (b < 1) b += a;
			while (b >= a) b -= a;
		} else {
			a = b;
			special = 'index';
		}
		switch (special){
			case 'n': parsed = {a: a, b: b, special: 'n'}; break;
			case 'odd': parsed = {a: 2, b: 0, special: 'n'}; break;
			case 'even': parsed = {a: 2, b: 1, special: 'n'}; break;
			case 'first': parsed = {a: 0, special: 'index'}; break;
			case 'last': parsed = {special: 'last-child'}; break;
			case 'only': parsed = {special: 'only-child'}; break;
			default: parsed = {a: (a - 1), special: 'index'};
		}

		return Selectors.Cache.nth[argument] = parsed;
	},

	parseSelector: function(selector){
		if (Selectors.Cache.parsed[selector]) return Selectors.Cache.parsed[selector];
		var m, parsed = {classes: [], pseudos: [], attributes: []};
		while ((m = Selectors.RegExps.combined.exec(selector))){
			var cn = m[1], an = m[2], ao = m[3], av = m[5], pn = m[6], pa = m[7];
			if (cn){
				parsed.classes.push(cn);
			} else if (pn){
				var parser = Selectors.Pseudo.get(pn);
				if (parser) parsed.pseudos.push({parser: parser, argument: pa});
				else parsed.attributes.push({name: pn, operator: '=', value: pa});
			} else if (an){
				parsed.attributes.push({name: an, operator: ao, value: av});
			}
		}
		if (!parsed.classes.length) delete parsed.classes;
		if (!parsed.attributes.length) delete parsed.attributes;
		if (!parsed.pseudos.length) delete parsed.pseudos;
		if (!parsed.classes && !parsed.attributes && !parsed.pseudos) parsed = null;
		return Selectors.Cache.parsed[selector] = parsed;
	},

	parseTagAndID: function(selector){
		var tag = selector.match(Selectors.RegExps.tag);
		var id = selector.match(Selectors.RegExps.id);
		return [(tag) ? tag[1] : '*', (id) ? id[1] : false];
	},

	filter: function(item, parsed, local){
		var i;
		if (parsed.classes){
			for (i = parsed.classes.length; i--; i){
				var cn = parsed.classes[i];
				if (!Selectors.Filters.byClass(item, cn)) return false;
			}
		}
		if (parsed.attributes){
			for (i = parsed.attributes.length; i--; i){
				var att = parsed.attributes[i];
				if (!Selectors.Filters.byAttribute(item, att.name, att.operator, att.value)) return false;
			}
		}
		if (parsed.pseudos){
			for (i = parsed.pseudos.length; i--; i){
				var psd = parsed.pseudos[i];
				if (!Selectors.Filters.byPseudo(item, psd.parser, psd.argument, local)) return false;
			}
		}
		return true;
	},

	getByTagAndID: function(ctx, tag, id){
		if (id){
			var item = (ctx.getElementById) ? ctx.getElementById(id, true) : Element.getElementById(ctx, id, true);
			return (item && Selectors.Filters.byTag(item, tag)) ? [item] : [];
		} else {
			return ctx.getElementsByTagName(tag);
		}
	},

	search: function(self, expression, local){
		var splitters = [];

		var selectors = expression.trim().replace(Selectors.RegExps.splitter, function(m0, m1, m2){
			splitters.push(m1);
			return ':)' + m2;
		}).split(':)');

		var items, filtered, item;

		for (var i = 0, l = selectors.length; i < l; i++){

			var selector = selectors[i];

			if (i == 0 && Selectors.RegExps.quick.test(selector)){
				items = self.getElementsByTagName(selector);
				continue;
			}

			var splitter = splitters[i - 1];

			var tagid = Selectors.Utils.parseTagAndID(selector);
			var tag = tagid[0], id = tagid[1];

			if (i == 0){
				items = Selectors.Utils.getByTagAndID(self, tag, id);
			} else {
				var uniques = {}, found = [];
				for (var j = 0, k = items.length; j < k; j++) found = Selectors.Getters[splitter](found, items[j], tag, id, uniques);
				items = found;
			}

			var parsed = Selectors.Utils.parseSelector(selector);

			if (parsed){
				filtered = [];
				for (var m = 0, n = items.length; m < n; m++){
					item = items[m];
					if (Selectors.Utils.filter(item, parsed, local)) filtered.push(item);
				}
				items = filtered;
			}

		}

		return items;

	}

};

Selectors.Getters = {

	' ': function(found, self, tag, id, uniques){
		var items = Selectors.Utils.getByTagAndID(self, tag, id);
		for (var i = 0, l = items.length; i < l; i++){
			var item = items[i];
			if (Selectors.Utils.chk(item, uniques)) found.push(item);
		}
		return found;
	},

	'>': function(found, self, tag, id, uniques){
		var children = Selectors.Utils.getByTagAndID(self, tag, id);
		for (var i = 0, l = children.length; i < l; i++){
			var child = children[i];
			if (child.parentNode == self && Selectors.Utils.chk(child, uniques)) found.push(child);
		}
		return found;
	},

	'+': function(found, self, tag, id, uniques){
		while ((self = self.nextSibling)){
			if (self.nodeType == 1){
				if (Selectors.Utils.chk(self, uniques) && Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
				break;
			}
		}
		return found;
	},

	'~': function(found, self, tag, id, uniques){
		while ((self = self.nextSibling)){
			if (self.nodeType == 1){
				if (!Selectors.Utils.chk(self, uniques)) break;
				if (Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
			}
		}
		return found;
	}

};

Selectors.Filters = {

	byTag: function(self, tag){
		return (tag == '*' || (self.tagName && self.tagName.toLowerCase() == tag));
	},

	byID: function(self, id){
		return (!id || (self.id && self.id == id));
	},

	byClass: function(self, klass){
		return (self.className && self.className.contains(klass, ' '));
	},

	byPseudo: function(self, parser, argument, local){
		return parser.call(self, argument, local);
	},

	byAttribute: function(self, name, operator, value){
		var result = Element.prototype.getProperty.call(self, name);
		if (!result) return (operator == '!=');
		if (!operator || value == undefined) return true;
		switch (operator){
			case '=': return (result == value);
			case '*=': return (result.contains(value));
			case '^=': return (result.substr(0, value.length) == value);
			case '$=': return (result.substr(result.length - value.length) == value);
			case '!=': return (result != value);
			case '~=': return result.contains(value, ' ');
			case '|=': return result.contains(value, '-');
		}
		return false;
	}

};

Selectors.Pseudo = new Hash({

	// w3c pseudo selectors

	checked: function(){
		return this.checked;
	},

	empty: function(){
		return !(this.innerText || this.textContent || '').length;
	},

	not: function(selector){
		return !Element.match(this, selector);
	},

	contains: function(text){
		return (this.innerText || this.textContent || '').contains(text);
	},

	'first-child': function(){
		return Selectors.Pseudo.index.call(this, 0);
	},

	'last-child': function(){
		var element = this;
		while ((element = element.nextSibling)){
			if (element.nodeType == 1) return false;
		}
		return true;
	},

	'only-child': function(){
		var prev = this;
		while ((prev = prev.previousSibling)){
			if (prev.nodeType == 1) return false;
		}
		var next = this;
		while ((next = next.nextSibling)){
			if (next.nodeType == 1) return false;
		}
		return true;
	},

	'nth-child': function(argument, local){
		argument = (argument == undefined) ? 'n' : argument;
		var parsed = Selectors.Utils.parseNthArgument(argument);
		if (parsed.special != 'n') return Selectors.Pseudo[parsed.special].call(this, parsed.a, local);
		var count = 0;
		local.positions = local.positions || {};
		var uid = $uid(this);
		if (!local.positions[uid]){
			var self = this;
			while ((self = self.previousSibling)){
				if (self.nodeType != 1) continue;
				count ++;
				var position = local.positions[$uid(self)];
				if (position != undefined){
					count = position + count;
					break;
				}
			}
			local.positions[uid] = count;
		}
		return (local.positions[uid] % parsed.a == parsed.b);
	},

	// custom pseudo selectors

	index: function(index){
		var element = this, count = 0;
		while ((element = element.previousSibling)){
			if (element.nodeType == 1 && ++count > index) return false;
		}
		return (count == index);
	},

	even: function(argument, local){
		return Selectors.Pseudo['nth-child'].call(this, '2n+1', local);
	},

	odd: function(argument, local){
		return Selectors.Pseudo['nth-child'].call(this, '2n', local);
	}

});


/*
Script: Domready.js
	Contains the domready custom event.

License:
	MIT-style license.
*/

Element.Events.domready = {

	onAdd: function(fn){
		if (Browser.loaded) fn.call(this);
	}

};

(function(){

	var domready = function(){
		if (Browser.loaded) return;
		Browser.loaded = true;
		window.fireEvent('domready');
		document.fireEvent('domready');
	};

	if (Browser.Engine.trident){
		var temp = document.createElement('div');
		(function(){
			($try(function(){
				temp.doScroll('left');
				return $(temp).inject(document.body).set('html', 'temp').dispose();
			})) ? domready() : arguments.callee.delay(50);
		})();
	} else if (Browser.Engine.webkit && Browser.Engine.version < 525){
		(function(){
			(['loaded', 'complete'].contains(document.readyState)) ? domready() : arguments.callee.delay(50);
		})();
	} else {
		window.addEvent('load', domready);
		document.addEvent('DOMContentLoaded', domready);
	}

})();


/*
Script: JSON.js
	JSON encoder and decoder.

License:
	MIT-style license.

See Also:
	<http://www.json.org/>
*/

var JSON = new Hash({

	$specialChars: {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'},

	$replaceChars: function(chr){
		return JSON.$specialChars[chr] || '\\u00' + Math.floor(chr.charCodeAt() / 16).toString(16) + (chr.charCodeAt() % 16).toString(16);
	},

	encode: function(obj){
		switch ($type(obj)){
			case 'string':
				return '"' + obj.replace(/[\x00-\x1f\\"]/g, JSON.$replaceChars) + '"';
			case 'array':
				return '[' + String(obj.map(JSON.encode).filter($defined)) + ']';
			case 'object': case 'hash':
				var string = [];
				Hash.each(obj, function(value, key){
					var json = JSON.encode(value);
					if (json) string.push(JSON.encode(key) + ':' + json);
				});
				return '{' + string + '}';
			case 'number': case 'boolean': return String(obj);
			case false: return 'null';
		}
		return null;
	},

	decode: function(string, secure){
		if ($type(string) != 'string' || !string.length) return null;
		if (secure && !(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) return null;
		return eval('(' + string + ')');
	}

});

Native.implement([Hash, Array, String, Number], {

	toJSON: function(){
		return JSON.encode(this);
	}

});


/*
Script: Cookie.js
	Class for creating, loading, and saving browser Cookies.

License:
	MIT-style license.

Credits:
	Based on the functions by Peter-Paul Koch (http://quirksmode.org).
*/

var Cookie = new Class({

	Implements: Options,

	options: {
		path: false,
		domain: false,
		duration: false,
		secure: false,
		document: document
	},

	initialize: function(key, options){
		this.key = key;
		this.setOptions(options);
	},

	write: function(value){
		value = encodeURIComponent(value);
		if (this.options.domain) value += '; domain=' + this.options.domain;
		if (this.options.path) value += '; path=' + this.options.path;
		if (this.options.duration){
			var date = new Date();
			date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);
			value += '; expires=' + date.toGMTString();
		}
		if (this.options.secure) value += '; secure';
		this.options.document.cookie = this.key + '=' + value;
		return this;
	},

	read: function(){
		var value = this.options.document.cookie.match('(?:^|;)\\s*' + this.key.escapeRegExp() + '=([^;]*)');
		return (value) ? decodeURIComponent(value[1]) : null;
	},

	dispose: function(){
		new Cookie(this.key, $merge(this.options, {duration: -1})).write('');
		return this;
	}

});

Cookie.write = function(key, value, options){
	return new Cookie(key, options).write(value);
};

Cookie.read = function(key){
	return new Cookie(key).read();
};

Cookie.dispose = function(key, options){
	return new Cookie(key, options).dispose();
};


/*
Script: Swiff.js
	Wrapper for embedding SWF movies. Supports (and fixes) External Interface Communication.

License:
	MIT-style license.

Credits:
	Flash detection & Internet Explorer + Flash Player 9 fix inspired by SWFObject.
*/

var Swiff = new Class({

	Implements: [Options],

	options: {
		id: null,
		height: 1,
		width: 1,
		container: null,
		properties: {},
		params: {
			quality: 'high',
			allowScriptAccess: 'always',
			wMode: 'transparent',
			swLiveConnect: true
		},
		callBacks: {},
		vars: {}
	},

	toElement: function(){
		return this.object;
	},

	initialize: function(path, options){
		this.instance = 'Swiff_' + $time();

		this.setOptions(options);
		options = this.options;
		var id = this.id = options.id || this.instance;
		var container = $(options.container);

		Swiff.CallBacks[this.instance] = {};

		var params = options.params, vars = options.vars, callBacks = options.callBacks;
		var properties = $extend({height: options.height, width: options.width}, options.properties);

		var self = this;

		for (var callBack in callBacks){
			Swiff.CallBacks[this.instance][callBack] = (function(option){
				return function(){
					return option.apply(self.object, arguments);
				};
			})(callBacks[callBack]);
			vars[callBack] = 'Swiff.CallBacks.' + this.instance + '.' + callBack;
		}

		params.flashVars = Hash.toQueryString(vars);
		if (Browser.Engine.trident){
			properties.classid = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000';
			params.movie = path;
		} else {
			properties.type = 'application/x-shockwave-flash';
			properties.data = path;
		}
		var build = '<object id="' + id + '"';
		for (var property in properties) build += ' ' + property + '="' + properties[property] + '"';
		build += '>';
		for (var param in params){
			if (params[param]) build += '<param name="' + param + '" value="' + params[param] + '" />';
		}
		build += '</object>';
		this.object = ((container) ? container.empty() : new Element('div')).set('html', build).firstChild;
	},

	replaces: function(element){
		element = $(element, true);
		element.parentNode.replaceChild(this.toElement(), element);
		return this;
	},

	inject: function(element){
		$(element, true).appendChild(this.toElement());
		return this;
	},

	remote: function(){
		return Swiff.remote.apply(Swiff, [this.toElement()].extend(arguments));
	}

});

Swiff.CallBacks = {};

Swiff.remote = function(obj, fn){
	var rs = obj.CallFunction('<invoke name="' + fn + '" returntype="javascript">' + __flash__argumentsToXML(arguments, 2) + '</invoke>');
	return eval(rs);
};


/*
Script: Fx.js
	Contains the basic animation logic to be extended by all other Fx Classes.

License:
	MIT-style license.
*/

var Fx = new Class({

	Implements: [Chain, Events, Options],

	options: {
		/*
		onStart: $empty,
		onCancel: $empty,
		onComplete: $empty,
		*/
		fps: 50,
		unit: false,
		duration: 500,
		link: 'ignore'
	},

	initialize: function(options){
		this.subject = this.subject || this;
		this.setOptions(options);
		this.options.duration = Fx.Durations[this.options.duration] || this.options.duration.toInt();
		var wait = this.options.wait;
		if (wait === false) this.options.link = 'cancel';
	},

	getTransition: function(){
		return function(p){
			return -(Math.cos(Math.PI * p) - 1) / 2;
		};
	},

	step: function(){
		var time = $time();
		if (time < this.time + this.options.duration){
			var delta = this.transition((time - this.time) / this.options.duration);
			this.set(this.compute(this.from, this.to, delta));
		} else {
			this.set(this.compute(this.from, this.to, 1));
			this.complete();
		}
	},

	set: function(now){
		return now;
	},

	compute: function(from, to, delta){
		return Fx.compute(from, to, delta);
	},

	check: function(caller){
		if (!this.timer) return true;
		switch (this.options.link){
			case 'cancel': this.cancel(); return true;
			case 'chain': this.chain(caller.bind(this, Array.slice(arguments, 1))); return false;
		}
		return false;
	},

	start: function(from, to){
		if (!this.check(arguments.callee, from, to)) return this;
		this.from = from;
		this.to = to;
		this.time = 0;
		this.transition = this.getTransition();
		this.startTimer();
		this.onStart();
		return this;
	},

	complete: function(){
		if (this.stopTimer()) this.onComplete();
		return this;
	},

	cancel: function(){
		if (this.stopTimer()) this.onCancel();
		return this;
	},

	onStart: function(){
		this.fireEvent('start', this.subject);
	},

	onComplete: function(){
		this.fireEvent('complete', this.subject);
		if (!this.callChain()) this.fireEvent('chainComplete', this.subject);
	},

	onCancel: function(){
		this.fireEvent('cancel', this.subject).clearChain();
	},

	pause: function(){
		this.stopTimer();
		return this;
	},

	resume: function(){
		this.startTimer();
		return this;
	},

	stopTimer: function(){
		if (!this.timer) return false;
		this.time = $time() - this.time;
		this.timer = $clear(this.timer);
		return true;
	},

	startTimer: function(){
		if (this.timer) return false;
		this.time = $time() - this.time;
		this.timer = this.step.periodical(Math.round(1000 / this.options.fps), this);
		return true;
	}

});

Fx.compute = function(from, to, delta){
	return (to - from) * delta + from;
};

Fx.Durations = {'short': 250, 'normal': 500, 'long': 1000};


/*
Script: Fx.CSS.js
	Contains the CSS animation logic. Used by Fx.Tween, Fx.Morph, Fx.Elements.

License:
	MIT-style license.
*/

Fx.CSS = new Class({

	Extends: Fx,

	//prepares the base from/to object

	prepare: function(element, property, values){
		values = $splat(values);
		var values1 = values[1];
		if (!$chk(values1)){
			values[1] = values[0];
			values[0] = element.getStyle(property);
		}
		var parsed = values.map(this.parse);
		return {from: parsed[0], to: parsed[1]};
	},

	//parses a value into an array

	parse: function(value){
		value = $lambda(value)();
		value = (typeof value == 'string') ? value.split(' ') : $splat(value);
		return value.map(function(val){
			val = String(val);
			var found = false;
			Fx.CSS.Parsers.each(function(parser, key){
				if (found) return;
				var parsed = parser.parse(val);
				if ($chk(parsed)) found = {value: parsed, parser: parser};
			});
			found = found || {value: val, parser: Fx.CSS.Parsers.String};
			return found;
		});
	},

	//computes by a from and to prepared objects, using their parsers.

	compute: function(from, to, delta){
		var computed = [];
		(Math.min(from.length, to.length)).times(function(i){
			computed.push({value: from[i].parser.compute(from[i].value, to[i].value, delta), parser: from[i].parser});
		});
		computed.$family = {name: 'fx:css:value'};
		return computed;
	},

	//serves the value as settable

	serve: function(value, unit){
		if ($type(value) != 'fx:css:value') value = this.parse(value);
		var returned = [];
		value.each(function(bit){
			returned = returned.concat(bit.parser.serve(bit.value, unit));
		});
		return returned;
	},

	//renders the change to an element

	render: function(element, property, value, unit){
		element.setStyle(property, this.serve(value, unit));
	},

	//searches inside the page css to find the values for a selector

	search: function(selector){
		if (Fx.CSS.Cache[selector]) return Fx.CSS.Cache[selector];
		var to = {};
		Array.each(document.styleSheets, function(sheet, j){
			var href = sheet.href;
			if (href && href.contains('://') && !href.contains(document.domain)) return;
			var rules = sheet.rules || sheet.cssRules;
			Array.each(rules, function(rule, i){
				if (!rule.style) return;
				var selectorText = (rule.selectorText) ? rule.selectorText.replace(/^\w+/, function(m){
					return m.toLowerCase();
				}) : null;
				if (!selectorText || !selectorText.test('^' + selector + '$')) return;
				Element.Styles.each(function(value, style){
					if (!rule.style[style] || Element.ShortStyles[style]) return;
					value = String(rule.style[style]);
					to[style] = (value.test(/^rgb/)) ? value.rgbToHex() : value;
				});
			});
		});
		return Fx.CSS.Cache[selector] = to;
	}

});

Fx.CSS.Cache = {};

Fx.CSS.Parsers = new Hash({

	Color: {
		parse: function(value){
			if (value.match(/^#[0-9a-f]{3,6}$/i)) return value.hexToRgb(true);
			return ((value = value.match(/(\d+),\s*(\d+),\s*(\d+)/))) ? [value[1], value[2], value[3]] : false;
		},
		compute: function(from, to, delta){
			return from.map(function(value, i){
				return Math.round(Fx.compute(from[i], to[i], delta));
			});
		},
		serve: function(value){
			return value.map(Number);
		}
	},

	Number: {
		parse: parseFloat,
		compute: Fx.compute,
		serve: function(value, unit){
			return (unit) ? value + unit : value;
		}
	},

	String: {
		parse: $lambda(false),
		compute: $arguments(1),
		serve: $arguments(0)
	}

});


/*
Script: Fx.Tween.js
	Formerly Fx.Style, effect to transition any CSS property for an element.

License:
	MIT-style license.
*/

Fx.Tween = new Class({

	Extends: Fx.CSS,

	initialize: function(element, options){
		this.element = this.subject = $(element);
		this.parent(options);
	},

	set: function(property, now){
		if (arguments.length == 1){
			now = property;
			property = this.property || this.options.property;
		}
		this.render(this.element, property, now, this.options.unit);
		return this;
	},

	start: function(property, from, to){
		if (!this.check(arguments.callee, property, from, to)) return this;
		var args = Array.flatten(arguments);
		this.property = this.options.property || args.shift();
		var parsed = this.prepare(this.element, this.property, args);
		return this.parent(parsed.from, parsed.to);
	}

});

Element.Properties.tween = {

	set: function(options){
		var tween = this.retrieve('tween');
		if (tween) tween.cancel();
		return this.eliminate('tween').store('tween:options', $extend({link: 'cancel'}, options));
	},

	get: function(options){
		if (options || !this.retrieve('tween')){
			if (options || !this.retrieve('tween:options')) this.set('tween', options);
			this.store('tween', new Fx.Tween(this, this.retrieve('tween:options')));
		}
		return this.retrieve('tween');
	}

};

Element.implement({

	tween: function(property, from, to){
		this.get('tween').start(arguments);
		return this;
	},

	fade: function(how){
		var fade = this.get('tween'), o = 'opacity', toggle;
		how = $pick(how, 'toggle');
		switch (how){
			case 'in': fade.start(o, 1); break;
			case 'out': fade.start(o, 0); break;
			case 'show': fade.set(o, 1); break;
			case 'hide': fade.set(o, 0); break;
			case 'toggle':
				var flag = this.retrieve('fade:flag', this.get('opacity') == 1);
				fade.start(o, (flag) ? 0 : 1);
				this.store('fade:flag', !flag);
				toggle = true;
			break;
			default: fade.start(o, arguments);
		}
		if (!toggle) this.eliminate('fade:flag');
		return this;
	},

	highlight: function(start, end){
		if (!end){
			end = this.retrieve('highlight:original', this.getStyle('background-color'));
			end = (end == 'transparent') ? '#fff' : end;
		}
		var tween = this.get('tween');
		tween.start('background-color', start || '#ffff88', end).chain(function(){
			this.setStyle('background-color', this.retrieve('highlight:original'));
			tween.callChain();
		}.bind(this));
		return this;
	}

});


/*
Script: Fx.Morph.js
	Formerly Fx.Styles, effect to transition any number of CSS properties for an element using an object of rules, or CSS based selector rules.

License:
	MIT-style license.
*/

Fx.Morph = new Class({

	Extends: Fx.CSS,

	initialize: function(element, options){
		this.element = this.subject = $(element);
		this.parent(options);
	},

	set: function(now){
		if (typeof now == 'string') now = this.search(now);
		for (var p in now) this.render(this.element, p, now[p], this.options.unit);
		return this;
	},

	compute: function(from, to, delta){
		var now = {};
		for (var p in from) now[p] = this.parent(from[p], to[p], delta);
		return now;
	},

	start: function(properties){
		if (!this.check(arguments.callee, properties)) return this;
		if (typeof properties == 'string') properties = this.search(properties);
		var from = {}, to = {};
		for (var p in properties){
			var parsed = this.prepare(this.element, p, properties[p]);
			from[p] = parsed.from;
			to[p] = parsed.to;
		}
		return this.parent(from, to);
	}

});

Element.Properties.morph = {

	set: function(options){
		var morph = this.retrieve('morph');
		if (morph) morph.cancel();
		return this.eliminate('morph').store('morph:options', $extend({link: 'cancel'}, options));
	},

	get: function(options){
		if (options || !this.retrieve('morph')){
			if (options || !this.retrieve('morph:options')) this.set('morph', options);
			this.store('morph', new Fx.Morph(this, this.retrieve('morph:options')));
		}
		return this.retrieve('morph');
	}

};

Element.implement({

	morph: function(props){
		this.get('morph').start(props);
		return this;
	}

});


/*
Script: Fx.Transitions.js
	Contains a set of advanced transitions to be used with any of the Fx Classes.

License:
	MIT-style license.

Credits:
	Easing Equations by Robert Penner, <http://www.robertpenner.com/easing/>, modified and optimized to be used with MooTools.
*/

Fx.implement({

	getTransition: function(){
		var trans = this.options.transition || Fx.Transitions.Sine.easeInOut;
		if (typeof trans == 'string'){
			var data = trans.split(':');
			trans = Fx.Transitions;
			trans = trans[data[0]] || trans[data[0].capitalize()];
			if (data[1]) trans = trans['ease' + data[1].capitalize() + (data[2] ? data[2].capitalize() : '')];
		}
		return trans;
	}

});

Fx.Transition = function(transition, params){
	params = $splat(params);
	return $extend(transition, {
		easeIn: function(pos){
			return transition(pos, params);
		},
		easeOut: function(pos){
			return 1 - transition(1 - pos, params);
		},
		easeInOut: function(pos){
			return (pos <= 0.5) ? transition(2 * pos, params) / 2 : (2 - transition(2 * (1 - pos), params)) / 2;
		}
	});
};

Fx.Transitions = new Hash({

	linear: $arguments(0)

});

Fx.Transitions.extend = function(transitions){
	for (var transition in transitions) Fx.Transitions[transition] = new Fx.Transition(transitions[transition]);
};

Fx.Transitions.extend({

	Pow: function(p, x){
		return Math.pow(p, x[0] || 6);
	},

	Expo: function(p){
		return Math.pow(2, 8 * (p - 1));
	},

	Circ: function(p){
		return 1 - Math.sin(Math.acos(p));
	},

	Sine: function(p){
		return 1 - Math.sin((1 - p) * Math.PI / 2);
	},

	Back: function(p, x){
		x = x[0] || 1.618;
		return Math.pow(p, 2) * ((x + 1) * p - x);
	},

	Bounce: function(p){
		var value;
		for (var a = 0, b = 1; 1; a += b, b /= 2){
			if (p >= (7 - 4 * a) / 11){
				value = b * b - Math.pow((11 - 6 * a - 11 * p) / 4, 2);
				break;
			}
		}
		return value;
	},

	Elastic: function(p, x){
		return Math.pow(2, 10 * --p) * Math.cos(20 * p * Math.PI * (x[0] || 1) / 3);
	}

});

['Quad', 'Cubic', 'Quart', 'Quint'].each(function(transition, i){
	Fx.Transitions[transition] = new Fx.Transition(function(p){
		return Math.pow(p, [i + 2]);
	});
});


/*
Script: Request.js
	Powerful all purpose Request Class. Uses XMLHTTPRequest.

License:
	MIT-style license.
*/

var Request = new Class({

	Implements: [Chain, Events, Options],

	options: {/*
		onRequest: $empty,
		onComplete: $empty,
		onCancel: $empty,
		onSuccess: $empty,
		onFailure: $empty,
		onException: $empty,*/
		url: '',
		data: '',
		headers: {
			'X-Requested-With': 'XMLHttpRequest',
			'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
		},
		async: true,
		format: false,
		method: 'post',
		link: 'ignore',
		isSuccess: null,
		emulation: true,
		urlEncoded: true,
		encoding: 'utf-8',
		evalScripts: false,
		evalResponse: false
	},

	initialize: function(options){
		this.xhr = new Browser.Request();
		this.setOptions(options);
		this.options.isSuccess = this.options.isSuccess || this.isSuccess;
		this.headers = new Hash(this.options.headers);
	},

	onStateChange: function(){
		if (this.xhr.readyState != 4 || !this.running) return;
		this.running = false;
		this.status = 0;
		$try(function(){
			this.status = this.xhr.status;
		}.bind(this));
		if (this.options.isSuccess.call(this, this.status)){
			this.response = {text: this.xhr.responseText, xml: this.xhr.responseXML};
			this.success(this.response.text, this.response.xml);
		} else {
			this.response = {text: null, xml: null};
			this.failure();
		}
		this.xhr.onreadystatechange = $empty;
	},

	isSuccess: function(){
		return ((this.status >= 200) && (this.status < 300));
	},

	processScripts: function(text){
		if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) return $exec(text);
		return text.stripScripts(this.options.evalScripts);
	},

	success: function(text, xml){
		this.onSuccess(this.processScripts(text), xml);
	},

	onSuccess: function(){
		this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
	},

	failure: function(){
		this.onFailure();
	},

	onFailure: function(){
		this.fireEvent('complete').fireEvent('failure', this.xhr);
	},

	setHeader: function(name, value){
		this.headers.set(name, value);
		return this;
	},

	getHeader: function(name){
		return $try(function(){
			return this.xhr.getResponseHeader(name);
		}.bind(this));
	},

	check: function(caller){
		if (!this.running) return true;
		switch (this.options.link){
			case 'cancel': this.cancel(); return true;
			case 'chain': this.chain(caller.bind(this, Array.slice(arguments, 1))); return false;
		}
		return false;
	},

	send: function(options){
		if (!this.check(arguments.callee, options)) return this;
		this.running = true;

		var type = $type(options);
		if (type == 'string' || type == 'element') options = {data: options};

		var old = this.options;
		options = $extend({data: old.data, url: old.url, method: old.method}, options);
		var data = options.data, url = options.url, method = options.method;

		switch ($type(data)){
			case 'element': data = $(data).toQueryString(); break;
			case 'object': case 'hash': data = Hash.toQueryString(data);
		}

		if (this.options.format){
			var format = 'format=' + this.options.format;
			data = (data) ? format + '&' + data : format;
		}

		if (this.options.emulation && ['put', 'delete'].contains(method)){
			var _method = '_method=' + method;
			data = (data) ? _method + '&' + data : _method;
			method = 'post';
		}

		if (this.options.urlEncoded && method == 'post'){
			var encoding = (this.options.encoding) ? '; charset=' + this.options.encoding : '';
			this.headers.set('Content-type', 'application/x-www-form-urlencoded' + encoding);
		}

		if (data && method == 'get'){
			url = url + (url.contains('?') ? '&' : '?') + data;
			data = null;
		}

		this.xhr.open(method.toUpperCase(), url, this.options.async);

		this.xhr.onreadystatechange = this.onStateChange.bind(this);

		this.headers.each(function(value, key){
			try {
				this.xhr.setRequestHeader(key, value);
			} catch (e){
				this.fireEvent('exception', [key, value]);
			}
		}, this);

		this.fireEvent('request');
		this.xhr.send(data);
		if (!this.options.async) this.onStateChange();
		return this;
	},

	cancel: function(){
		if (!this.running) return this;
		this.running = false;
		this.xhr.abort();
		this.xhr.onreadystatechange = $empty;
		this.xhr = new Browser.Request();
		this.fireEvent('cancel');
		return this;
	}

});

(function(){

var methods = {};
['get', 'post', 'put', 'delete', 'GET', 'POST', 'PUT', 'DELETE'].each(function(method){
	methods[method] = function(){
		var params = Array.link(arguments, {url: String.type, data: $defined});
		return this.send($extend(params, {method: method.toLowerCase()}));
	};
});

Request.implement(methods);

})();

Element.Properties.send = {

	set: function(options){
		var send = this.retrieve('send');
		if (send) send.cancel();
		return this.eliminate('send').store('send:options', $extend({
			data: this, link: 'cancel', method: this.get('method') || 'post', url: this.get('action')
		}, options));
	},

	get: function(options){
		if (options || !this.retrieve('send')){
			if (options || !this.retrieve('send:options')) this.set('send', options);
			this.store('send', new Request(this.retrieve('send:options')));
		}
		return this.retrieve('send');
	}

};

Element.implement({

	send: function(url){
		var sender = this.get('send');
		sender.send({data: this, url: url || sender.options.url});
		return this;
	}

});


/*
Script: Request.HTML.js
	Extends the basic Request Class with additional methods for interacting with HTML responses.

License:
	MIT-style license.
*/

Request.HTML = new Class({

	Extends: Request,

	options: {
		update: false,
		evalScripts: true,
		filter: false
	},

	processHTML: function(text){
		var match = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
		text = (match) ? match[1] : text;

		var container = new Element('div');

		return $try(function(){
			var root = '<root>' + text + '</root>', doc;
			if (Browser.Engine.trident){
				doc = new ActiveXObject('Microsoft.XMLDOM');
				doc.async = false;
				doc.loadXML(root);
			} else {
				doc = new DOMParser().parseFromString(root, 'text/xml');
			}
			root = doc.getElementsByTagName('root')[0];
			for (var i = 0, k = root.childNodes.length; i < k; i++){
				var child = Element.clone(root.childNodes[i], true, true);
				if (child) container.grab(child);
			}
			return container;
		}) || container.set('html', text);
	},

	success: function(text){
		var options = this.options, response = this.response;

		response.html = text.stripScripts(function(script){
			response.javascript = script;
		});

		var temp = this.processHTML(response.html);

		response.tree = temp.childNodes;
		response.elements = temp.getElements('*');

		if (options.filter) response.tree = response.elements.filter(options.filter);
		if (options.update) $(options.update).empty().set('html', response.html);
		if (options.evalScripts) $exec(response.javascript);

		this.onSuccess(response.tree, response.elements, response.html, response.javascript);
	}

});

Element.Properties.load = {

	set: function(options){
		var load = this.retrieve('load');
		if (load) load.cancel();
		return this.eliminate('load').store('load:options', $extend({data: this, link: 'cancel', update: this, method: 'get'}, options));
	},

	get: function(options){
		if (options || ! this.retrieve('load')){
			if (options || !this.retrieve('load:options')) this.set('load', options);
			this.store('load', new Request.HTML(this.retrieve('load:options')));
		}
		return this.retrieve('load');
	}

};

Element.implement({

	load: function(){
		this.get('load').send(Array.link(arguments, {data: Object.type, url: String.type}));
		return this;
	}

});


/*
Script: Request.JSON.js
	Extends the basic Request Class with additional methods for sending and receiving JSON data.

License:
	MIT-style license.
*/

Request.JSON = new Class({

	Extends: Request,

	options: {
		secure: true
	},

	initialize: function(options){
		this.parent(options);
		this.headers.extend({'Accept': 'application/json', 'X-Request': 'JSON'});
	},

	success: function(text){
		this.response.json = JSON.decode(text, this.options.secure);
		this.onSuccess(this.response.json, text);
	}

});

//MooTools More, <http://mootools.net/more>. Copyright (c) 2006-2008 Valerio Proietti, <http://mad4milk.net>, MIT Style License.

/*
Script: Fx.Slide.js
	Effect to slide an element in and out of view.

License:
	MIT-style license.
*/

Fx.Slide = new Class({

	Extends: Fx,

	options: {
		mode: 'vertical'
	},

	initialize: function(element, options){
		this.addEvent('complete', function(){
			this.open = (this.wrapper['offset' + this.layout.capitalize()] != 0);
			if (this.open && Browser.Engine.webkit419) this.element.dispose().inject(this.wrapper);
		}, true);
		this.element = this.subject = $(element);
		this.parent(options);
		var wrapper = this.element.retrieve('wrapper');
		this.wrapper = wrapper || new Element('div', {
			styles: $extend(this.element.getStyles('margin', 'position'), {'overflow': 'hidden'})
		}).wraps(this.element);
		this.element.store('wrapper', this.wrapper).setStyle('margin', 0);
		this.now = [];
		this.open = true;
	},

	vertical: function(){
		this.margin = 'margin-top';
		this.layout = 'height';
		this.offset = this.element.offsetHeight;
	},

	horizontal: function(){
		this.margin = 'margin-left';
		this.layout = 'width';
		this.offset = this.element.offsetWidth;
	},

	set: function(now){
		this.element.setStyle(this.margin, now[0]);
		this.wrapper.setStyle(this.layout, now[1]);
		return this;
	},

	compute: function(from, to, delta){
		var now = [];
		var x = 2;
		x.times(function(i){
			now[i] = Fx.compute(from[i], to[i], delta);
		});
		return now;
	},

	start: function(how, mode){
		if (!this.check(arguments.callee, how, mode)) return this;
		this[mode || this.options.mode]();
		var margin = this.element.getStyle(this.margin).toInt();
		var layout = this.wrapper.getStyle(this.layout).toInt();
		var caseIn = [[margin, layout], [0, this.offset]];
		var caseOut = [[margin, layout], [-this.offset, 0]];
		var start;
		switch (how){
			case 'in': start = caseIn; break;
			case 'out': start = caseOut; break;
			case 'toggle': start = (this.wrapper['offset' + this.layout.capitalize()] == 0) ? caseIn : caseOut;
		}
		return this.parent(start[0], start[1]);
	},

	slideIn: function(mode){
		return this.start('in', mode);
	},

	slideOut: function(mode){
		return this.start('out', mode);
	},

	hide: function(mode){
		this[mode || this.options.mode]();
		this.open = false;
		return this.set([-this.offset, 0]);
	},

	show: function(mode){
		this[mode || this.options.mode]();
		this.open = true;
		return this.set([0, this.offset]);
	},

	toggle: function(mode){
		return this.start('toggle', mode);
	}

});

Element.Properties.slide = {

	set: function(options){
		var slide = this.retrieve('slide');
		if (slide) slide.cancel();
		return this.eliminate('slide').store('slide:options', $extend({link: 'cancel'}, options));
	},
	
	get: function(options){
		if (options || !this.retrieve('slide')){
			if (options || !this.retrieve('slide:options')) this.set('slide', options);
			this.store('slide', new Fx.Slide(this, this.retrieve('slide:options')));
		}
		return this.retrieve('slide');
	}

};

Element.implement({

	slide: function(how, mode){
		how = how || 'toggle';
		var slide = this.get('slide'), toggle;
		switch (how){
			case 'hide': slide.hide(mode); break;
			case 'show': slide.show(mode); break;
			case 'toggle':
				var flag = this.retrieve('slide:flag', slide.open);
				slide[(flag) ? 'slideOut' : 'slideIn'](mode);
				this.store('slide:flag', !flag);
				toggle = true;
			break;
			default: slide.start(how, mode);
		}
		if (!toggle) this.eliminate('slide:flag');
		return this;
	}

});


/*
Script: Fx.Scroll.js
	Effect to smoothly scroll any element, including the window.

License:
	MIT-style license.
*/

Fx.Scroll = new Class({

	Extends: Fx,

	options: {
		offset: {'x': 0, 'y': 0},
		wheelStops: true
	},

	initialize: function(element, options){
		this.element = this.subject = $(element);
		this.parent(options);
		var cancel = this.cancel.bind(this, false);

		if ($type(this.element) != 'element') this.element = $(this.element.getDocument().body);

		var stopper = this.element;

		if (this.options.wheelStops){
			this.addEvent('start', function(){
				stopper.addEvent('mousewheel', cancel);
			}, true);
			this.addEvent('complete', function(){
				stopper.removeEvent('mousewheel', cancel);
			}, true);
		}
	},

	set: function(){
		var now = Array.flatten(arguments);
		this.element.scrollTo(now[0], now[1]);
	},

	compute: function(from, to, delta){
		var now = [];
		var x = 2;
		x.times(function(i){
			now.push(Fx.compute(from[i], to[i], delta));
		});
		return now;
	},

	start: function(x, y){
		if (!this.check(arguments.callee, x, y)) return this;
		var offsetSize = this.element.getSize(), scrollSize = this.element.getScrollSize();
		var scroll = this.element.getScroll(), values = {x: x, y: y};
		for (var z in values){
			var max = scrollSize[z] - offsetSize[z];
			if ($chk(values[z])) values[z] = ($type(values[z]) == 'number') ? values[z].limit(0, max) : max;
			else values[z] = scroll[z];
			values[z] += this.options.offset[z];
		}
		return this.parent([scroll.x, scroll.y], [values.x, values.y]);
	},

	toTop: function(){
		return this.start(false, 0);
	},

	toLeft: function(){
		return this.start(0, false);
	},

	toRight: function(){
		return this.start('right', false);
	},

	toBottom: function(){
		return this.start(false, 'bottom');
	},

	toElement: function(el){
		var position = $(el).getPosition(this.element);
		return this.start(position.x, position.y);
	}

});


/*
Script: Fx.Elements.js
	Effect to change any number of CSS properties of any number of Elements.

License:
	MIT-style license.
*/

Fx.Elements = new Class({

	Extends: Fx.CSS,

	initialize: function(elements, options){
		this.elements = this.subject = $$(elements);
		this.parent(options);
	},

	compute: function(from, to, delta){
		var now = {};
		for (var i in from){
			var iFrom = from[i], iTo = to[i], iNow = now[i] = {};
			for (var p in iFrom) iNow[p] = this.parent(iFrom[p], iTo[p], delta);
		}
		return now;
	},

	set: function(now){
		for (var i in now){
			var iNow = now[i];
			for (var p in iNow) this.render(this.elements[i], p, iNow[p], this.options.unit);
		}
		return this;
	},

	start: function(obj){
		if (!this.check(arguments.callee, obj)) return this;
		var from = {}, to = {};
		for (var i in obj){
			var iProps = obj[i], iFrom = from[i] = {}, iTo = to[i] = {};
			for (var p in iProps){
				var parsed = this.prepare(this.elements[i], p, iProps[p]);
				iFrom[p] = parsed.from;
				iTo[p] = parsed.to;
			}
		}
		return this.parent(from, to);
	}

});

/*
Script: Drag.js
	The base Drag Class. Can be used to drag and resize Elements using mouse events.

License:
	MIT-style license.
*/

var Drag = new Class({

	Implements: [Events, Options],

	options: {/*
		onBeforeStart: $empty,
		onStart: $empty,
		onDrag: $empty,
		onCancel: $empty,
		onComplete: $empty,*/
		snap: 6,
		unit: 'px',
		grid: false,
		style: true,
		limit: false,
		handle: false,
		invert: false,
		preventDefault: false,
		modifiers: {x: 'left', y: 'top'}
	},

	initialize: function(){
		var params = Array.link(arguments, {'options': Object.type, 'element': $defined});
		this.element = $(params.element);
		this.document = this.element.getDocument();
		this.setOptions(params.options || {});
		var htype = $type(this.options.handle);
		this.handles = (htype == 'array' || htype == 'collection') ? $$(this.options.handle) : $(this.options.handle) || this.element;
		this.mouse = {'now': {}, 'pos': {}};
		this.value = {'start': {}, 'now': {}};
		
		this.selection = (Browser.Engine.trident) ? 'selectstart' : 'mousedown';
		
		this.bound = {
			start: this.start.bind(this),
			check: this.check.bind(this),
			drag: this.drag.bind(this),
			stop: this.stop.bind(this),
			cancel: this.cancel.bind(this),
			eventStop: $lambda(false)
		};
		this.attach();
	},

	attach: function(){
		this.handles.addEvent('mousedown', this.bound.start);
		return this;
	},

	detach: function(){
		this.handles.removeEvent('mousedown', this.bound.start);
		return this;
	},

	start: function(event){
		if (this.options.preventDefault) event.preventDefault();
		this.fireEvent('beforeStart', this.element);
		this.mouse.start = event.page;
		var limit = this.options.limit;
		this.limit = {'x': [], 'y': []};
		for (var z in this.options.modifiers){
			if (!this.options.modifiers[z]) continue;
			if (this.options.style) this.value.now[z] = this.element.getStyle(this.options.modifiers[z]).toInt();
			else this.value.now[z] = this.element[this.options.modifiers[z]];
			if (this.options.invert) this.value.now[z] *= -1;
			this.mouse.pos[z] = event.page[z] - this.value.now[z];
			if (limit && limit[z]){
				for (var i = 2; i--; i){
					if ($chk(limit[z][i])) this.limit[z][i] = $lambda(limit[z][i])();
				}
			}
		}
		if ($type(this.options.grid) == 'number') this.options.grid = {'x': this.options.grid, 'y': this.options.grid};
		this.document.addEvents({mousemove: this.bound.check, mouseup: this.bound.cancel});
		this.document.addEvent(this.selection, this.bound.eventStop);
	},

	check: function(event){
		if (this.options.preventDefault) event.preventDefault();
		var distance = Math.round(Math.sqrt(Math.pow(event.page.x - this.mouse.start.x, 2) + Math.pow(event.page.y - this.mouse.start.y, 2)));
		if (distance > this.options.snap){
      // this.cancel();
			this.document.addEvents({
				mousemove: this.bound.drag,
				mouseup: this.bound.stop
			});
			this.fireEvent('start', this.element).fireEvent('snap', this.element);
		}
	},

	drag: function(event){
		if (this.options.preventDefault) event.preventDefault();
		this.mouse.now = event.page;
		for (var z in this.options.modifiers){
			if (!this.options.modifiers[z]) continue;
			this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
			if (this.options.invert) this.value.now[z] *= -1;
			if (this.options.limit && this.limit[z]){
				if ($chk(this.limit[z][1]) && (this.value.now[z] > this.limit[z][1])){
					this.value.now[z] = this.limit[z][1];
				} else if ($chk(this.limit[z][0]) && (this.value.now[z] < this.limit[z][0])){
					this.value.now[z] = this.limit[z][0];
				}
			}
			if (this.options.grid[z]) this.value.now[z] -= (this.value.now[z] % this.options.grid[z]);
			if (this.options.style) this.element.setStyle(this.options.modifiers[z], this.value.now[z] + this.options.unit);
			else this.element[this.options.modifiers[z]] = this.value.now[z];
		}
		this.fireEvent('drag', this.element);
	},

	cancel: function(event){
		this.document.removeEvent('mousemove', this.bound.check);
		this.document.removeEvent('mouseup', this.bound.cancel);
		if (event){
			this.document.removeEvent(this.selection, this.bound.eventStop);
			this.fireEvent('cancel', this.element);
		}
	},

	stop: function(event){
		this.document.removeEvent(this.selection, this.bound.eventStop);
		this.document.removeEvent('mousemove', this.bound.drag);
		this.document.removeEvent('mouseup', this.bound.stop);
		if (event) this.fireEvent('complete', this.element);
	}

});

Element.implement({
	
	makeResizable: function(options){
		return new Drag(this, $merge({modifiers: {'x': 'width', 'y': 'height'}}, options));
	}

});

/*
Script: Drag.Move.js
	A Drag extension that provides support for the constraining of draggables to containers and droppables.

License:
	MIT-style license.
*/

Drag.Move = new Class({

	Extends: Drag,

	options: {
		droppables: [],
		container: false
	},

	initialize: function(element, options){
		this.parent(element, options);
		this.droppables = $$(this.options.droppables);
		this.container = $(this.options.container);
		if (this.container && $type(this.container) != 'element') this.container = $(this.container.getDocument().body);
		element = this.element;
		
		var current = element.getStyle('position');
		var position = (current != 'static') ? current : 'absolute';
		if (element.getStyle('left') == 'auto' || element.getStyle('top') == 'auto') element.position(element.getPosition(element.offsetParent));
		
		element.setStyle('position', position);
		
		this.addEvent('start', function(){
			this.checkDroppables();
		}, true);
	},

	start: function(event){
		if (this.container){
			var el = this.element, cont = this.container, ccoo = cont.getCoordinates(el.offsetParent), cps = {}, ems = {};

			['top', 'right', 'bottom', 'left'].each(function(pad){
				cps[pad] = cont.getStyle('padding-' + pad).toInt();
				ems[pad] = el.getStyle('margin-' + pad).toInt();
			}, this);

			var width = el.offsetWidth + ems.left + ems.right, height = el.offsetHeight + ems.top + ems.bottom;
			var x = [ccoo.left + cps.left, ccoo.right - cps.right - width];
			var y = [ccoo.top + cps.top, ccoo.bottom - cps.bottom - height];

			this.options.limit = {x: x, y: y};
		}
		this.parent(event);
	},

	checkAgainst: function(el){
		el = el.getCoordinates();
		var now = this.mouse.now;
		return (now.x > el.left && now.x < el.right && now.y < el.bottom && now.y > el.top);
	},

	checkDroppables: function(){
		var overed = this.droppables.filter(this.checkAgainst, this).getLast();
		if (this.overed != overed){
			if (this.overed) this.fireEvent('leave', [this.element, this.overed]);
			if (overed){
				this.overed = overed;
				this.fireEvent('enter', [this.element, overed]);
			} else {
				this.overed = null;
			}
		}
	},

	drag: function(event){
		this.parent(event);
		if (this.droppables.length) this.checkDroppables();
	},

	stop: function(event){
		this.checkDroppables();
		this.fireEvent('drop', [this.element, this.overed]);
		this.overed = null;
		return this.parent(event);
	}

});

Element.implement({

	makeDraggable: function(options){
		return new Drag.Move(this, options);
	}

});


/*
Script: Hash.Cookie.js
	Class for creating, reading, and deleting Cookies in JSON format.

License:
	MIT-style license.
*/

Hash.Cookie = new Class({

	Extends: Cookie,

	options: {
		autoSave: true
	},

	initialize: function(name, options){
		this.parent(name, options);
		this.load();
	},

	save: function(){
		var value = JSON.encode(this.hash);
		if (!value || value.length > 4096) return false; //cookie would be truncated!
		if (value == '{}') this.dispose();
		else this.write(value);
		return true;
	},

	load: function(){
		this.hash = new Hash(JSON.decode(this.read(), true));
		return this;
	}

});

Hash.Cookie.implement((function(){
	
	var methods = {};
	
	Hash.each(Hash.prototype, function(method, name){
		methods[name] = function(){
			var value = method.apply(this.hash, arguments);
			if (this.options.autoSave) this.save();
			return value;
		};
	});
	
	return methods;
	
})());

/*
Script: Color.js
	Class for creating and manipulating colors in JavaScript. Supports HSB -> RGB Conversions and vice versa.

License:
	MIT-style license.
*/

var Color = new Native({
  
	initialize: function(color, type){
		if (arguments.length >= 3){
			type = "rgb"; color = Array.slice(arguments, 0, 3);
		} else if (typeof color == 'string'){
			if (color.match(/rgb/)) color = color.rgbToHex().hexToRgb(true);
			else if (color.match(/hsb/)) color = color.hsbToRgb();
			else color = color.hexToRgb(true);
		}
		type = type || 'rgb';
		switch (type){
			case 'hsb':
				var old = color;
				color = color.hsbToRgb();
				color.hsb = old;
			break;
			case 'hex': color = color.hexToRgb(true); break;
		}
		color.rgb = color.slice(0, 3);
		color.hsb = color.hsb || color.rgbToHsb();
		color.hex = color.rgbToHex();
		return $extend(color, this);
	}

});

Color.implement({

	mix: function(){
		var colors = Array.slice(arguments);
		var alpha = ($type(colors.getLast()) == 'number') ? colors.pop() : 50;
		var rgb = this.slice();
		colors.each(function(color){
			color = new Color(color);
			for (var i = 0; i < 3; i++) rgb[i] = Math.round((rgb[i] / 100 * (100 - alpha)) + (color[i] / 100 * alpha));
		});
		return new Color(rgb, 'rgb');
	},

	invert: function(){
		return new Color(this.map(function(value){
			return 255 - value;
		}));
	},

	setHue: function(value){
		return new Color([value, this.hsb[1], this.hsb[2]], 'hsb');
	},

	setSaturation: function(percent){
		return new Color([this.hsb[0], percent, this.hsb[2]], 'hsb');
	},

	setBrightness: function(percent){
		return new Color([this.hsb[0], this.hsb[1], percent], 'hsb');
	}

});

function $RGB(r, g, b){
	return new Color([r, g, b], 'rgb');
};

function $HSB(h, s, b){
	return new Color([h, s, b], 'hsb');
};

function $HEX(hex){
	return new Color(hex, 'hex');
};

Array.implement({

	rgbToHsb: function(){
		var red = this[0], green = this[1], blue = this[2];
		var hue, saturation, brightness;
		var max = Math.max(red, green, blue), min = Math.min(red, green, blue);
		var delta = max - min;
		brightness = max / 255;
		saturation = (max != 0) ? delta / max : 0;
		if (saturation == 0){
			hue = 0;
		} else {
			var rr = (max - red) / delta;
			var gr = (max - green) / delta;
			var br = (max - blue) / delta;
			if (red == max) hue = br - gr;
			else if (green == max) hue = 2 + rr - br;
			else hue = 4 + gr - rr;
			hue /= 6;
			if (hue < 0) hue++;
		}
		return [Math.round(hue * 360), Math.round(saturation * 100), Math.round(brightness * 100)];
	},

	hsbToRgb: function(){
		var br = Math.round(this[2] / 100 * 255);
		if (this[1] == 0){
			return [br, br, br];
		} else {
			var hue = this[0] % 360;
			var f = hue % 60;
			var p = Math.round((this[2] * (100 - this[1])) / 10000 * 255);
			var q = Math.round((this[2] * (6000 - this[1] * f)) / 600000 * 255);
			var t = Math.round((this[2] * (6000 - this[1] * (60 - f))) / 600000 * 255);
			switch (Math.floor(hue / 60)){
				case 0: return [br, t, p];
				case 1: return [q, br, p];
				case 2: return [p, br, t];
				case 3: return [p, q, br];
				case 4: return [t, p, br];
				case 5: return [br, p, q];
			}
		}
		return false;
	}

});

String.implement({

	rgbToHsb: function(){
		var rgb = this.match(/\d{1,3}/g);
		return (rgb) ? hsb.rgbToHsb() : null;
	},
	
	hsbToRgb: function(){
		var hsb = this.match(/\d{1,3}/g);
		return (hsb) ? hsb.hsbToRgb() : null;
	}

});


/*
Script: Group.js
	Class for monitoring collections of events

License:
	MIT-style license.
*/

var Group = new Class({

	initialize: function(){
		this.instances = Array.flatten(arguments);
		this.events = {};
		this.checker = {};
	},

	addEvent: function(type, fn){
		this.checker[type] = this.checker[type] || {};
		this.events[type] = this.events[type] || [];
		if (this.events[type].contains(fn)) return false;
		else this.events[type].push(fn);
		this.instances.each(function(instance, i){
			instance.addEvent(type, this.check.bind(this, [type, instance, i]));
		}, this);
		return this;
	},

	check: function(type, instance, i){
		this.checker[type][i] = true;
		var every = this.instances.every(function(current, j){
			return this.checker[type][j] || false;
		}, this);
		if (!every) return;
		this.checker[type] = {};
		this.events[type].each(function(event){
			event.call(this, this.instances, instance);
		}, this);
	}

});


/*
Script: Assets.js
	Provides methods to dynamically load JavaScript, CSS, and Image files into the document.

License:
	MIT-style license.
*/

var Asset = new Hash({

	javascript: function(source, properties){
		properties = $extend({
			onload: $empty,
			document: document,
			check: $lambda(true)
		}, properties);
		
		var script = new Element('script', {'src': source, 'type': 'text/javascript'});
		
		var load = properties.onload.bind(script), check = properties.check, doc = properties.document;
		delete properties.onload; delete properties.check; delete properties.document;
		
		script.addEvents({
			load: load,
			readystatechange: function(){
				if (['loaded', 'complete'].contains(this.readyState)) load();
			}
		}).setProperties(properties);
		
		
		if (Browser.Engine.webkit419) var checker = (function(){
			if (!$try(check)) return;
			$clear(checker);
			load();
		}).periodical(50);
		
		return script.inject(doc.head);
	},

	css: function(source, properties){
		return new Element('link', $merge({
			'rel': 'stylesheet', 'media': 'screen', 'type': 'text/css', 'href': source
		}, properties)).inject(document.head);
	},

	image: function(source, properties){
		properties = $merge({
			'onload': $empty,
			'onabort': $empty,
			'onerror': $empty
		}, properties);
		var image = new Image();
		var element = $(image) || new Element('img');
		['load', 'abort', 'error'].each(function(name){
			var type = 'on' + name;
			var event = properties[type];
			delete properties[type];
			image[type] = function(){
				if (!image) return;
				if (!element.parentNode){
					element.width = image.width;
					element.height = image.height;
				}
				image = image.onload = image.onabort = image.onerror = null;
				event.delay(1, element, element);
				element.fireEvent(name, element, 1);
			};
		});
		image.src = element.src = source;
		if (image && image.complete) image.onload.delay(1);
		return element.setProperties(properties);
	},

	images: function(sources, options){
		options = $merge({
			onComplete: $empty,
			onProgress: $empty
		}, options);
		if (!sources.push) sources = [sources];
		var images = [];
		var counter = 0;
		sources.each(function(source){
			var img = new Asset.image(source, {
				'onload': function(){
					options.onProgress.call(this, counter, sources.indexOf(source));
					counter++;
					if (counter == sources.length) options.onComplete();
				}
			});
			images.push(img);
		});
		return new Elements(images);
	}

});

/*
Script: Sortables.js
	Class for creating a drag and drop sorting interface for lists of items.

License:
	MIT-style license.
*/

var Sortables = new Class({

	Implements: [Events, Options],

	options: {/*
		onSort: $empty,
		onStart: $empty,
		onComplete: $empty,*/
		snap: 4,
		opacity: 1,
		clone: false,
		revert: false,
		handle: false,
		constrain: false
	},

	initialize: function(lists, options){
		this.setOptions(options);
		this.elements = [];
		this.lists = [];
		this.idle = true;
		
		this.addLists($$($(lists) || lists));
		if (!this.options.clone) this.options.revert = false;
		if (this.options.revert) this.effect = new Fx.Morph(null, $merge({duration: 250, link: 'cancel'}, this.options.revert));
	},

	attach: function(){
		this.addLists(this.lists);
		return this;
	},

	detach: function(){
		this.lists = this.removeLists(this.lists);
		return this;
	},

	addItems: function(){
		Array.flatten(arguments).each(function(element){
			this.elements.push(element);
			var start = element.retrieve('sortables:start', this.start.bindWithEvent(this, element));
			(this.options.handle ? element.getElement(this.options.handle) || element : element).addEvent('mousedown', start);
		}, this);
		return this;
	},

	addLists: function(){
		Array.flatten(arguments).each(function(list){
			this.lists.push(list);
			this.addItems(list.getChildren());
		}, this);
		return this;
	},

	removeItems: function(){
		var elements = [];
		Array.flatten(arguments).each(function(element){
			elements.push(element);
			this.elements.erase(element);
			var start = element.retrieve('sortables:start');
			(this.options.handle ? element.getElement(this.options.handle) || element : element).removeEvent('mousedown', start);
		}, this);
		return $$(elements);
	},

	removeLists: function(){
		var lists = [];
		Array.flatten(arguments).each(function(list){
			lists.push(list);
			this.lists.erase(list);
			this.removeItems(list.getChildren());
		}, this);
		return $$(lists);
	},

	getClone: function(event, element){
		if (!this.options.clone) return new Element('div').inject(document.body);
		if ($type(this.options.clone) == 'function') return this.options.clone.call(this, event, element, this.list);
		return element.clone(true).setStyles({
			'margin': '0px',
			'position': 'absolute',
			'visibility': 'hidden',
			'width': element.getStyle('width')
		}).inject(this.list).position(element.getPosition(element.getOffsetParent()));
	},

	getDroppables: function(){
		var droppables = this.list.getChildren();
		if (!this.options.constrain) droppables = this.lists.concat(droppables).erase(this.list);
		return droppables.erase(this.clone).erase(this.element);
	},

	insert: function(dragging, element){
		var where = 'inside';
		if (this.lists.contains(element)){
			this.list = element;
			this.drag.droppables = this.getDroppables();
		} else {
			where = this.element.getAllPrevious().contains(element) ? 'before' : 'after';
		}
		this.element.inject(element, where);
		this.fireEvent('sort', [this.element, this.clone]);
	},

	start: function(event, element){
		if (!this.idle) return;
		this.idle = false;
		this.element = element;
		this.opacity = element.get('opacity');
		this.list = element.getParent();
		this.clone = this.getClone(event, element);
		
		this.drag = new Drag.Move(this.clone, {
			snap: this.options.snap,
			container: this.options.constrain && this.element.getParent(),
			droppables: this.getDroppables(),
			onSnap: function(){
				event.stop();
				this.clone.setStyle('visibility', 'visible');
				this.element.set('opacity', this.options.opacity || 0);
				this.fireEvent('start', [this.element, this.clone]);
			}.bind(this),
			onEnter: this.insert.bind(this),
			onCancel: this.reset.bind(this),
			onComplete: this.end.bind(this)
		});
		
		this.clone.inject(this.element, 'before');
		this.drag.start(event);
	},

	end: function(){
		this.drag.detach();
		this.element.set('opacity', this.opacity);
		if (this.effect){
			var dim = this.element.getStyles('width', 'height');
			var pos = this.clone.computePosition(this.element.getPosition(this.clone.offsetParent));
			this.effect.element = this.clone;
			this.effect.start({
				top: pos.top,
				left: pos.left,
				width: dim.width,
				height: dim.height,
				opacity: 0.25
			}).chain(this.reset.bind(this));
		} else {
			this.reset();
		}
	},

	reset: function(){
		this.idle = true;
		this.clone.destroy();
		this.fireEvent('complete', this.element);
	},

	serialize: function(){
		var params = Array.link(arguments, {modifier: Function.type, index: $defined});
		var serial = this.lists.map(function(list){
			return list.getChildren().map(params.modifier || function(element){
				return element.get('id');
			}, this);
		}, this);
		
		var index = params.index;
		if (this.lists.length == 1) index = 0;
		return $chk(index) && index >= 0 && index < this.lists.length ? serial[index] : serial;
	}

});

/*
Script: Tips.js
	Class for creating nice tips that follow the mouse cursor when hovering an element.

License:
	MIT-style license.
*/

var Tips = new Class({

	Implements: [Events, Options],

	options: {
		onShow: function(tip){
			tip.setStyle('visibility', 'visible');
		},
		onHide: function(tip){
			tip.setStyle('visibility', 'hidden');
		},
		showDelay: 100,
		hideDelay: 100,
		className: null,
		offsets: {x: 16, y: 16},
		fixed: false
	},

	initialize: function(){
		var params = Array.link(arguments, {options: Object.type, elements: $defined});
		this.setOptions(params.options || null);
		
		this.tip = new Element('div').inject(document.body);
		
		if (this.options.className) this.tip.addClass(this.options.className);
		
		var top = new Element('div', {'class': 'tip-top'}).inject(this.tip);
		this.container = new Element('div', {'class': 'tip'}).inject(this.tip);
		var bottom = new Element('div', {'class': 'tip-bottom'}).inject(this.tip);

		this.tip.setStyles({position: 'absolute', top: 0, left: 0, visibility: 'hidden'});
		
		if (params.elements) this.attach(params.elements);
	},
	
	attach: function(elements){
		$$(elements).each(function(element){
			var title = element.retrieve('tip:title', element.get('title'));
			var text = element.retrieve('tip:text', element.get('rel') || element.get('href'));
			var enter = element.retrieve('tip:enter', this.elementEnter.bindWithEvent(this, element));
			var leave = element.retrieve('tip:leave', this.elementLeave.bindWithEvent(this, element));
			element.addEvents({mouseenter: enter, mouseleave: leave});
			if (!this.options.fixed){
				var move = element.retrieve('tip:move', this.elementMove.bindWithEvent(this, element));
				element.addEvent('mousemove', move);
			}
			element.store('tip:native', element.get('title'));
			element.erase('title');
		}, this);
		return this;
	},
	
	detach: function(elements){
		$$(elements).each(function(element){
			element.removeEvent('mouseenter', element.retrieve('tip:enter') || $empty);
			element.removeEvent('mouseleave', element.retrieve('tip:leave') || $empty);
			element.removeEvent('mousemove', element.retrieve('tip:move') || $empty);
			element.eliminate('tip:enter').eliminate('tip:leave').eliminate('tip:move');
			var original = element.retrieve('tip:native');
			if (original) element.set('title', original);
		});
		return this;
	},
	
	elementEnter: function(event, element){
		
		$A(this.container.childNodes).each(Element.dispose);
		
		var title = element.retrieve('tip:title');
		
		if (title){
			this.titleElement = new Element('div', {'class': 'tip-title'}).inject(this.container);
			this.fill(this.titleElement, title);
		}
		
		var text = element.retrieve('tip:text');
		if (text){
			this.textElement = new Element('div', {'class': 'tip-text'}).inject(this.container);
			this.fill(this.textElement, text);
		}
		
		this.timer = $clear(this.timer);
		this.timer = this.show.delay(this.options.showDelay, this);

		this.position((!this.options.fixed) ? event : {page: element.getPosition()});
	},
	
	elementLeave: function(event){
		$clear(this.timer);
		this.timer = this.hide.delay(this.options.hideDelay, this);
	},
	
	elementMove: function(event){
		this.position(event);
	},
	
	position: function(event){
		var size = window.getSize(), scroll = window.getScroll();
		var tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight};
		var props = {x: 'left', y: 'top'};
		for (var z in props){
			var pos = event.page[z] + this.options.offsets[z];
			if ((pos + tip[z] - scroll[z]) > size[z]) pos = event.page[z] - this.options.offsets[z] - tip[z];
			this.tip.setStyle(props[z], pos);
		}
	},
	
	fill: function(element, contents){
		(typeof contents == 'string') ? element.set('html', contents) : element.adopt(contents);
	},

	show: function(){
		this.fireEvent('show', this.tip);
	},

	hide: function(){
		this.fireEvent('hide', this.tip);
	}

});

/*
Script: SmoothScroll.js
	Class for creating a smooth scrolling effect to all internal links on the page.

License:
	MIT-style license.
*/

var SmoothScroll = new Class({

	Extends: Fx.Scroll,

	initialize: function(options, context){
		context = context || document;
		var doc = context.getDocument(), win = context.getWindow();
		this.parent(doc, options);
		this.links = (this.options.links) ? $$(this.options.links) : $$(doc.links);
		var location = win.location.href.match(/^[^#]*/)[0] + '#';
		this.links.each(function(link){
			if (link.href.indexOf(location) != 0) return;
			var anchor = link.href.substr(location.length);
			if (anchor && $(anchor)) this.useLink(link, anchor);
		}, this);
		if (!Browser.Engine.webkit419) this.addEvent('complete', function(){
			win.location.hash = this.anchor;
		}, true);
	},

	useLink: function(link, anchor){
		link.addEvent('click', function(event){
			this.anchor = anchor;
			this.toElement(anchor);
			event.stop();
		}.bind(this));
	}

});

/*
Script: Slider.js
	Class for creating horizontal and vertical slider controls.

License:
	MIT-style license.
*/

var Slider = new Class({

	Implements: [Events, Options],

	options: {/*
		onChange: $empty,
		onComplete: $empty,*/
		onTick: function(position){
			if(this.options.snap) position = this.toPosition(this.step);
			this.knob.setStyle(this.property, position);
		},
		snap: false,
		offset: 0,
		range: false,
		wheel: false,
		steps: 100,
		mode: 'horizontal'
	},

	initialize: function(element, knob, options){
		this.setOptions(options);
		this.element = $(element);
		this.knob = $(knob);
		this.previousChange = this.previousEnd = this.step = -1;
		this.element.addEvent('mousedown', this.clickedElement.bind(this));
		if (this.options.wheel) this.element.addEvent('mousewheel', this.scrolledElement.bindWithEvent(this));
		var offset, limit = {}, modifiers = {'x': false, 'y': false};
		switch (this.options.mode){
			case 'vertical':
				this.axis = 'y';
				this.property = 'top';
				offset = 'offsetHeight';
				break;
			case 'horizontal':
				this.axis = 'x';
				this.property = 'left';
				offset = 'offsetWidth';
		}
		this.half = this.knob[offset] / 2;
		this.full = this.element[offset] - this.knob[offset] + (this.options.offset * 2);
		this.min = $chk(this.options.range[0]) ? this.options.range[0] : 0;
		this.max = $chk(this.options.range[1]) ? this.options.range[1] : this.options.steps;
		this.range = this.max - this.min;
		this.steps = this.options.steps || this.full;
		this.stepSize = Math.abs(this.range) / this.steps;
		this.stepWidth = this.stepSize * this.full / Math.abs(this.range) ;
		
		this.knob.setStyle('position', 'relative').setStyle(this.property, - this.options.offset);
		modifiers[this.axis] = this.property;
		limit[this.axis] = [- this.options.offset, this.full - this.options.offset];
		this.drag = new Drag(this.knob, {
			snap: 0,
			limit: limit,
			modifiers: modifiers,
			onDrag: this.draggedKnob.bind(this),
			onStart: this.draggedKnob.bind(this),
			onComplete: function(){
				this.draggedKnob();
				this.end();
			}.bind(this)
		});
		if (this.options.snap) {
			this.drag.options.grid = Math.ceil(this.stepWidth);
			this.drag.options.limit[this.axis][1] = this.full;
		}
	},

	set: function(step){
		if (!((this.range > 0) ^ (step < this.min))) step = this.min;
		if (!((this.range > 0) ^ (step > this.max))) step = this.max;
		
		this.step = Math.round(step);
		this.checkStep();
		this.end();
		this.fireEvent('tick', this.toPosition(this.step));
		return this;
	},

	clickedElement: function(event){
		var dir = this.range < 0 ? -1 : 1;
		var position = event.page[this.axis] - this.element.getPosition()[this.axis] - this.half;
		position = position.limit(-this.options.offset, this.full -this.options.offset);
		
		this.step = Math.round(this.min + dir * this.toStep(position));
		this.checkStep();
		this.end();
		this.fireEvent('tick', position);
	},
	
	scrolledElement: function(event){
		var mode = (this.options.mode == 'horizontal') ? (event.wheel < 0) : (event.wheel > 0);
		this.set(mode ? this.step - this.stepSize : this.step + this.stepSize);
		event.stop();
	},

	draggedKnob: function(){
		var dir = this.range < 0 ? -1 : 1;
		var position = this.drag.value.now[this.axis];
		position = position.limit(-this.options.offset, this.full -this.options.offset);
		this.step = Math.round(this.min + dir * this.toStep(position));
		this.checkStep();
	},

	checkStep: function(){
		if (this.previousChange != this.step){
			this.previousChange = this.step;
			this.fireEvent('change', this.step);
		}
	},

	end: function(){
		if (this.previousEnd !== this.step){
			this.previousEnd = this.step;
			this.fireEvent('complete', this.step + '');
		}
	},

	toStep: function(position){
		var step = (position + this.options.offset) * this.stepSize / this.full * this.steps;
		return this.options.steps ? Math.round(step -= step % this.stepSize) : step;
	},

	toPosition: function(step){
		return (this.full * Math.abs(this.min - step)) / (this.steps * this.stepSize) - this.options.offset;
	}

});

/*
Script: Scroller.js
	Class which scrolls the contents of any Element (including the window) when the mouse reaches the Element's boundaries.

License:
	MIT-style license.
*/

var Scroller = new Class({

	Implements: [Events, Options],

	options: {
		area: 20,
		velocity: 1,
		onChange: function(x, y){
			this.element.scrollTo(x, y);
		}
	},

	initialize: function(element, options){
		this.setOptions(options);
		this.element = $(element);
		this.listener = ($type(this.element) != 'element') ? $(this.element.getDocument().body) : this.element;
		this.timer = null;
		this.coord = this.getCoords.bind(this);
		this.pos = this.element.getPosition();
	},

	start: function(){
		this.listener.addEvent('mousemove', this.coord);
	},

	stop: function(){
		this.listener.removeEvent('mousemove', this.coord);
		this.timer = $clear(this.timer);
	},

	getCoords: function(event){
		this.page = (this.listener.get('tag') == 'body') ? event.client : event.page;
		if (!this.timer) this.timer = this.scroll.periodical(50, this);
	},

	scroll: function(){
		var size = this.element.getSize(), scroll = this.element.getScroll(), change = {'x': 0, 'y': 0};
		for (var z in this.page){
			if (this.page[z] < (this.options.area + this.pos[z]) && scroll[z] != 0)
				change[z] = (this.page[z] - this.options.area - this.pos[z]) * this.options.velocity;
			else if (this.page[z] + this.options.area > (size[z] + this.pos[z]) && size[z] + size[z] != scroll[z])
				change[z] = (this.page[z] - size[z] + this.options.area - this.pos[z]) * this.options.velocity;
		}
		if (change.y || change.x) this.fireEvent('change', [scroll.x + change.x, scroll.y + change.y]);
	}

});

/*
Script: Accordion.js
	An Fx.Elements extension which allows you to easily create accordion type controls.

License:
	MIT-style license.
*/

var Accordion = new Class({

	Extends: Fx.Elements,

	options: {/*
		onActive: $empty,
		onBackground: $empty,*/
		display: 0,
		show: false,
		height: true,
		width: false,
		opacity: true,
		fixedHeight: false,
		fixedWidth: false,
		wait: false,
		alwaysHide: false
	},

	initialize: function(){
		var params = Array.link(arguments, {'container': Element.type, 'options': Object.type, 'togglers': $defined, 'elements': $defined});
		this.parent(params.elements, params.options);
		this.togglers = $$(params.togglers);
		this.container = $(params.container);
		this.previous = -1;
		if (this.options.alwaysHide) this.options.wait = true;
		if ($chk(this.options.show)){
			this.options.display = false;
			this.previous = this.options.show;
		}
		if (this.options.start){
			this.options.display = false;
			this.options.show = false;
		}
		this.effects = {};
		if (this.options.opacity) this.effects.opacity = 'fullOpacity';
		if (this.options.width) this.effects.width = this.options.fixedWidth ? 'fullWidth' : 'offsetWidth';
		if (this.options.height) this.effects.height = this.options.fixedHeight ? 'fullHeight' : 'scrollHeight';
		for (var i = 0, l = this.togglers.length; i < l; i++) this.addSection(this.togglers[i], this.elements[i]);
		this.elements.each(function(el, i){
			if (this.options.show === i){
				this.fireEvent('active', [this.togglers[i], el]);
			} else {
				for (var fx in this.effects) el.setStyle(fx, 0);
			}
		}, this);
		if ($chk(this.options.display)) this.display(this.options.display);
	},

	addSection: function(toggler, element, pos){
		toggler = $(toggler);
		element = $(element);
		var test = this.togglers.contains(toggler);
		var len = this.togglers.length;
		this.togglers.include(toggler);
		this.elements.include(element);
		if (len && (!test || pos)){
			pos = $pick(pos, len - 1);
			toggler.inject(this.togglers[pos], 'before');
			element.inject(toggler, 'after');
		} else if (this.container && !test){
			toggler.inject(this.container);
			element.inject(this.container);
		}
		var idx = this.togglers.indexOf(toggler);
		toggler.addEvent('click', this.display.bind(this, idx));
		if (this.options.height) element.setStyles({'padding-top': 0, 'border-top': 'none', 'padding-bottom': 0, 'border-bottom': 'none'});
		if (this.options.width) element.setStyles({'padding-left': 0, 'border-left': 'none', 'padding-right': 0, 'border-right': 'none'});
		element.fullOpacity = 1;
		if (this.options.fixedWidth) element.fullWidth = this.options.fixedWidth;
		if (this.options.fixedHeight) element.fullHeight = this.options.fixedHeight;
		element.setStyle('overflow', 'hidden');
		if (!test){
			for (var fx in this.effects) element.setStyle(fx, 0);
		}
		return this;
	},

	display: function(index){
		index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
		if ((this.timer && this.options.wait) || (index === this.previous && !this.options.alwaysHide)) return this;
		this.previous = index;
		var obj = {};
		this.elements.each(function(el, i){
			obj[i] = {};
			var hide = (i != index) || (this.options.alwaysHide && (el.offsetHeight > 0));
			this.fireEvent(hide ? 'background' : 'active', [this.togglers[i], el]);
			for (var fx in this.effects) obj[i][fx] = hide ? 0 : el[this.effects[fx]];
		}, this);
		return this.start(obj);
	}

});
/*
Script: Clientcide.js
	The Clientcide namespace.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var Clientcide = {
	version: '763',
	setAssetLocation: function(baseHref) {
		if (window.StickyWin && StickyWin.ui) {
			StickyWin.UI.refactor({
				options: {
					baseHref: baseHref + '/stickyWinHTML/'
				}
			});
			if (StickyWin.alert) {
				var CGFsimpleErrorPopup = StickyWin.alert.bind(window);
				StickyWin.alert = function(msghdr, msg, base) {
				    return CGFsimpleErrorPopup(msghdr, msg, base||baseHref + "/simple.error.popup");
				};
			}
		}
		if (window.TagMaker) {
			TagMaker = TagMaker.refactor({
			    options: {
			        baseHref: baseHref + '/tips/'
			    }
			});
		}
		if (window.ProductPicker) {
			ProductPicker.refactor({
			    options:{
			        baseHref: baseHref + '/Picker'
			    }
			});
		}

		if (window.Autocompleter) {
			var AcClientcide = {
					options: {
						baseHref: baseHref + '/autocompleter/'
					}
			};
			Autocompleter.Base.refactor(AcClientcide);
			if (Autocompleter.Ajax) {
				["Base", "Xhtml", "Json"].each(function(c){
					if(Autocompleter.Ajax[c]) Autocompleter.Ajax[c].refactor(AcClientcide);
				});
			}
			if (Autocompleter.Local) Autocompleter.Local.refactor(AcClientcide);
			if (Autocompleter.JsonP) Autocompleter.JsonP.refactor(AcClientcide);
		}

		if (window.Lightbox) {
			Lightbox.refactor({
			    options: {
			        assetBaseUrl: baseHref + '/slimbox/'
			    }
			});
		}

		if (window.Waiter) {
			Waiter.refactor({
				options: {
					baseHref: baseHref + '/waiter/'
				}
			});
		}
	},
	preLoadCss: function(){
		if (window.DatePicker) new DatePicker();
		if (window.ProductPicker) new ProductPicker();
		if (window.TagMaker) new TagMaker();
		if (window.StickyWin && StickyWin.ui) StickyWin.ui();
		if (window.StickyWin && StickyWin.pointy) StickyWin.pointy();
		Clientcide.preloaded = true;
		return true;
	},
	preloaded: false
};
(function(){
	if (!window.addEvent) return;
	var preload = function(){
		if (window.dbug) dbug.log('preloading clientcide css');
		if (!Clientcide.preloaded) Clientcide.preLoadCss();
	};
	window.addEvent('domready', preload);
	window.addEvent('load', preload);
})();
setCNETAssetBaseHref = Clientcide.setAssetLocation;

/*
Script: dbug.js
	A wrapper for Firebug console.* statements.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var dbug = {
	logged: [],	
	timers: {},
	firebug: false, 
	enabled: false, 
	log: function() {
		dbug.logged.push(arguments);
	},
	nolog: function(msg) {
		dbug.logged.push(arguments);
	},
	time: function(name){
		dbug.timers[name] = new Date().getTime();
	},
	timeEnd: function(name){
		if (dbug.timers[name]) {
			var end = new Date().getTime() - dbug.timers[name];
			dbug.timers[name] = false;
			dbug.log('%s: %s', name, end);
		} else dbug.log('no such timer: %s', name);
	},
	enable: function(silent) { 
		if(dbug.firebug) {
			try {
				dbug.enabled = true;
				dbug.log = function(){
						(console.debug || console.log).apply(console, arguments);
				};
				dbug.time = function(){
					console.time.apply(console, arguments);
				};
				dbug.timeEnd = function(){
					console.timeEnd.apply(console, arguments);
				};
				if(!silent) dbug.log('enabling dbug');
				for(var i=0;i<dbug.logged.length;i++){ dbug.log.apply(console, dbug.logged[i]); }
				dbug.logged=[];
			} catch(e) {
				dbug.enable.delay(400);
			}
		}
	},
	disable: function(){ 
		if(dbug.firebug) dbug.enabled = false;
		dbug.log = dbug.nolog;
		dbug.time = function(){};
		dbug.timeEnd = function(){};
	},
	cookie: function(set){
		var value = document.cookie.match('(?:^|;)\\s*jsdebug=([^;]*)');
		var debugCookie = value ? unescape(value[1]) : false;
		if((!$defined(set) && debugCookie != 'true') || ($defined(set) && set)) {
			dbug.enable();
			dbug.log('setting debugging cookie');
			var date = new Date();
			date.setTime(date.getTime()+(24*60*60*1000));
			document.cookie = 'jsdebug=true;expires='+date.toGMTString()+';path=/;';
		} else dbug.disableCookie();
	},
	disableCookie: function(){
		dbug.log('disabling debugging cookie');
		document.cookie = 'jsdebug=false;path=/;';
	}
};

(function(){
	var fb = typeof console != "undefined";
	var debugMethods = ['debug','info','warn','error','assert','dir','dirxml'];
	var otherMethods = ['trace','group','groupEnd','profile','profileEnd','count'];
	function set(methodList, defaultFunction) {
		for(var i = 0; i < methodList.length; i++){
			dbug[methodList[i]] = (fb && console[methodList[i]])?console[methodList[i]]:defaultFunction;
		}
	};
	set(debugMethods, dbug.log);
	set(otherMethods, function(){});
})();
if (typeof console != "undefined" && console.warn){
	dbug.firebug = true;
	var value = document.cookie.match('(?:^|;)\\s*jsdebug=([^;]*)');
	var debugCookie = value ? unescape(value[1]) : false;
	if(window.location.href.indexOf("jsdebug=true")>0 || debugCookie=='true') dbug.enable();
	if(debugCookie=='true')dbug.log('debugging cookie enabled');
	if(window.location.href.indexOf("jsdebugCookie=true")>0){
		dbug.cookie();
		if(!dbug.enabled)dbug.enable();
	}
	if(window.location.href.indexOf("jsdebugCookie=false")>0)dbug.disableCookie();
}


/*
Script: Class.Refactor.js
	Extends a class onto itself with new property, preserving any items attached to the class's namespace.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
Class.refactor = function(orig, props) {
	props = $extend($unlink(props), { Extends: orig	});
	var update = new Class(props);
	$each(orig, function(v, k) {
		update[k] = update[k] || v;
	});
	return update;
};

$extend(Class.prototype, { 
	refactor: function(props){ 
		this.prototype = Class.refactor(this, props).prototype;
		return this;
	} 
});

/*
Script: ToElement.js
	Defines the toElement method for a class.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var ToElement = new Class({
	toElement: function(){
		return this.element;
	}
});

/*
Script: Browser.Extras.js
	Extends the Window native object to include methods useful in managing the window location and urls.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

$extend(Browser, {
	getHost:function(url){
		url = $pick(url, window.location.href);
		var host = url;
		if(url.test('http://')){
			url = url.substring(url.indexOf('http://')+7,url.length);
			if(url.test(':')) url = url.substring(0, url.indexOf(":"));
			if(url.test('/')) return url.substring(0,url.indexOf('/'));
			return url;
		}
		return false;
	},
	getQueryStringValue: function(key, url) {
		try { 
			return Browser.getQueryStringValues(url)[key];
		}catch(e){return null;}
	},
	getQueryStringValues: function(url){
		var qs = $pick(url, window.location.search, '').split('?')[1]; //get the query string
		if (!$chk(qs)) return {};
		if (qs.test('#')) qs = qs.substring(0, qs.indexOf('#'));
		try {
      if (qs) return qs.parseQuery();
		} catch(e){
			return null;
		}
		return {}; //if there isn't one, return null
	},
	getPort: function(url) {
		url = $pick(url, window.location.href);
		var re = new RegExp(':([0-9]{4})');
		var m = re.exec(url);
	  if (m == null) return false;
	  else {
			var port = false;
			m.each(function(val){
				if($chk(parseInt(val))) port = val;
			});
	  }
		return port;
	},
  redraw: function(element){
    var n = document.createTextNode(' ');
    this.adopt(n);
    (function(){n.dispose()}).delay(1);
    return this;
	}
});
window.addEvent('domready', function(){
	var count = 0;
	//this is in case domready fires before string.extras loads
	function setQs(){
		function retry(){
			count++;
			if (count < 20) setQs.delay(50);
		}; 
		try {
			if (!Browser.getQueryStringValues()) retry();
			else Browser.qs = Browser.getQueryStringValues();
		} catch(e){
			retry();
		}
	}
	setQs();
});

/*
Script: IframeShim.js
	Defines IframeShim, a class for obscuring select lists and flash objects in IE.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/	
var IframeShim = new Class({
	Implements: [Options, Events],
	options: {
		name: '',
		className:'iframeShim',
		display:false,
		zindex: null,
		margin: 0,
		offset: {
			x: 0,
			y: 0
		},
		browsers: (Browser.Engine.trident4 || (Browser.Engine.gecko && !Browser.Engine.gecko19 && Browser.Platform.mac))
	},
	initialize: function (element, options){
		this.setOptions(options);
		//legacy
		if(this.options.offset && this.options.offset.top) this.options.offset.y = this.options.offset.top;
		if(this.options.offset && this.options.offset.left) this.options.offset.x = this.options.offset.left;
		this.element = $(element);
		this.makeShim();
		return;
	},
	makeShim: function(){
		this.shim = new Element('iframe');
		this.id = this.options.name || new Date().getTime() + "_shim";
		if(!this.options.browsers) return;
		if(this.element.getStyle('z-Index').toInt()<1 || isNaN(this.element.getStyle('z-Index').toInt()))
			this.element.setStyle('z-Index',5);
		var z = this.element.getStyle('z-Index')-1;
		
		if($chk(this.options.zindex) && 
			 this.element.getStyle('z-Index').toInt() > this.options.zindex)
			 z = this.options.zindex;
			
 		this.shim.setStyles({
			'position': 'absolute',
			'zIndex': z,
			'border': 'none',
			'filter': 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)'
		}).setProperties({
			'src': (window.location.protocol == 'https') ? '://0' : 'javascript:void(0)',
			'frameborder':'0',
			'scrolling':'no',
			'id':this.id
		}).addClass(this.options.className);
		
		this.element.store('shim', this);

		var inject = function(){
			this.shim.inject(this.element, 'after');
			if(this.options.display) this.show();
			else this.hide();
			this.fireEvent('onInject');
		};
		if(this.options.browsers){
			if(Browser.Engine.trident && !IframeShim.ready) {
				window.addEvent('load', inject.bind(this));
			} else {
				inject.run(null, this);
			}
		}
	},
	position: function(shim){
		if(!this.options.browsers || !IframeShim.ready) return this;
		var putItBack = this.element.expose();
		var size = this.element.getSize();
		putItBack();
		if($type(this.options.margin)){
			size.x = size.x-(this.options.margin*2);
			size.y = size.y-(this.options.margin*2);
			this.options.offset.x += this.options.margin; 
			this.options.offset.y += this.options.margin;
		}
 		this.shim.setStyles({
			'width': size.x,
			'height': size.y
		}).setPosition({
			relativeTo: this.element,
			offset: this.options.offset
		});
		return this;
	},
	hide: function(){
		if(this.options.browsers) this.shim.setStyle('display','none');
		return this;
	},
	show: function(){
		if(!this.options.browsers) return this;
		this.shim.setStyle('display','block');
		return this.position();
	},
	dispose: function(){
		if(this.options.browsers) this.shim.dispose();
		return this;
	}
});
window.addEvent('load', function(){
	IframeShim.ready = true;
});


/*
Script: Date.js
	Extends the Date native object to include methods useful in managing dates.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

new Native({name: 'Date', initialize: Date, protect: true});
['now','parse','UTC'].each(function(method){
	Native.genericize(Date, method, true);
});
Date.$Methods = new Hash();
["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds", "Time", "TimezoneOffset", 
	"Week", "Timezone", "GMTOffset", "DayOfYear", "LastMonth", "UTCDate", "UTCDay", "UTCFullYear",
	"AMPM", "UTCHours", "UTCMilliseconds", "UTCMinutes", "UTCMonth", "UTCSeconds"].each(function(method) {
	Date.$Methods.set(method.toLowerCase(), method);
});
$each({
	ms: "Milliseconds",
	year: "FullYear",
	min: "Minutes",
	mo: "Month",
	sec: "Seconds",
	hr: "Hours"
}, function(value, key){
	Date.$Methods.set(key, value);
});


Date.implement({
	set: function(key, value) {
		key = key.toLowerCase();
		var m = Date.$Methods;
		if (m.has(key)) this['set'+m.get(key)](value);
		return this;
	},
	get: function(key) {
		key = key.toLowerCase();
		var m = Date.$Methods;
		if (m.has(key)) return this['get'+m.get(key)]();
		return null;
	},
	clone: function() {
		return new Date(this.get('time'));
	},
	increment: function(interval, times) {
		return this.multiply(interval, times);
	},
	decrement: function(interval, times) {
		return this.multiply(interval, times, false);
	},
	multiply: function(interval, times, increment){
		interval = interval || 'day';
		times = $pick(times, 1);
		increment = $pick(increment, true);
		var multiplier = increment?1:-1;
		var month = this.format("%m").toInt()-1;
		var year = this.format("%Y").toInt();
		var time = this.get('time');
		var offset = 0;
		switch (interval) {
				case 'year':
					times.times(function(val) {
						if (Date.isLeapYear(year+val) && month > 1 && multiplier > 0) val++;
						if (Date.isLeapYear(year+val) && month <= 1 && multiplier < 0) val--;
						offset += Date.$units.year(year+val);
					});
					break;
				case 'month':
					times.times(function(val){
						if (multiplier < 0) val++;
						var mo = month+(val*multiplier);
						var year = year;
						if (mo < 0) {
							year--;
							mo = 12+mo;
						}
						if (mo > 11 || mo < 0) {
							year += (mo/12).toInt()*multiplier;
							mo = mo%12;
						}
						offset += Date.$units.month(mo, year);
					});
					break;
				case 'day':
					return this.set('date', this.get('date')+(multiplier*times));
				default:
					offset = Date.$units[interval]()*times;
					break;
		}
		this.set('time', time+(offset*multiplier));
		return this;
	},
	isLeapYear: function() {
		return Date.isLeapYear(this.get('year'));
	},
	clearTime: function() {
		['hr', 'min', 'sec', 'ms'].each(function(t){
			this.set(t, 0);
		}, this);
		return this;
	},
	diff: function(d, resolution) {
		resolution = resolution || 'day';
		if($type(d) == 'string') d = Date.parse(d);
		switch (resolution) {
			case 'year':
				return d.format("%Y").toInt() - this.format("%Y").toInt();
				break;
			case 'month':
				var months = (d.format("%Y").toInt() - this.format("%Y").toInt())*12;
				return months + d.format("%m").toInt() - this.format("%m").toInt();
				break;
			default:
				var diff = d.get('time') - this.get('time');
				if (diff < 0 && Date.$units[resolution]() > (-1*(diff))) return 0;
				else if (diff >= 0 && diff < Date.$units[resolution]()) return 0;
				return ((d.get('time') - this.get('time')) / Date.$units[resolution]()).round();
		}
	},
	getWeek: function() {
		var day = (new Date(this.get('year'), 0, 1)).get('date');
		return Math.round((this.get('dayofyear') + (day > 3 ? day - 4 : day + 3)) / 7);
	},
	getTimezone: function() {
		return this.toString()
			.replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/, '$1')
			.replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/, '$1$2$3');
	},
	getGMTOffset: function() {
		var off = this.get('timezoneOffset');
		return ((off > 0) ? '-' : '+')
			+ Math.floor(Math.abs(off) / 60).zeroise(2)
			+ (off % 60).zeroise(2);
	},
	parse: function(str) {
		this.set('time', Date.parse(str));
		return this;
	},
	format: function(f) {
		f = f || "%x %X";
		if (!this.valueOf()) return 'invalid date';
		//replace short-hand with actual format
		if (Date.$formats[f.toLowerCase()]) f = Date.$formats[f.toLowerCase()];
		var d = this;
		return f.replace(/\%([aAbBcdHIjmMpSUWwxXyYTZ])/g,
			function($1, $2) {
				switch ($2) {
					case 'a': return Date.$days[d.get('day')].substr(0, 3);
					case 'A': return Date.$days[d.get('day')];
					case 'b': return Date.$months[d.get('month')].substr(0, 3);
					case 'B': return Date.$months[d.get('month')];
					case 'c': return d.toString();
					case 'd': return d.get('date').zeroise(2);
					case 'H': return d.get('hr').zeroise(2);
					case 'I': return ((d.get('hr') % 12) || 12);
					case 'j': return d.get('dayofyear').zeroise(3);
					case 'm': return (d.get('mo') + 1).zeroise(2);
					case 'M': return d.get('min').zeroise(2);
					case 'p': return d.get('hr') < 12 ? 'AM' : 'PM';
					case 'S': return d.get('seconds').zeroise(2);
					case 'U': return d.get('week').zeroise(2);
					case 'W': throw new Error('%W is not supported yet');
					case 'w': return d.get('day');
					case 'x': 
						var c = Date.$cultures[Date.$culture];
						//return d.format("%{0}{3}%{1}{3}%{2}".substitute(c.map(function(s){return s.substr(0,1)}))); //grr!
						return d.format('%' + c[0].substr(0,1) +
							c[3] + '%' + c[1].substr(0,1) +
							c[3] + '%' + c[2].substr(0,1).toUpperCase());
					case 'X': return d.format('%I:%M%p');
					case 'y': return d.get('year').toString().substr(2);
					case 'Y': return d.get('year');
					case 'T': return d.get('GMTOffset');
					case 'Z': return d.get('Timezone');
					case '%': return '%';
				}
				return $2;
			}
		);
	},
	setAMPM: function(ampm){
		ampm = ampm.toUpperCase();
		if (this.format("%H").toInt() > 11 && ampm == "AM") 
			return this.decrement('hour', 12);
		else if (this.format("%H").toInt() < 12 && ampm == "PM")
			return this.increment('hour', 12);
		return this;
	}
});

Date.prototype.compare = Date.prototype.diff;
Date.prototype.strftime = Date.prototype.format;

Date.$nativeParse = Date.parse;

$extend(Date, {
	$months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
	$days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
	$daysInMonth: function(monthIndex, year) {
		if (Date.isLeapYear(year.toInt()) && monthIndex === 1) return 29;
		return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][monthIndex];
	},
	$epoch: -1,
	$era: -2,
	$units: {
		ms: function(){return 1},
		second: function(){return 1000},
		minute: function(){return 60000},
		hour: function(){return 3600000},
		day: function(){return 86400000},
		week: function(){return 608400000},
		month: function(monthIndex, year) {
			var d = new Date();
			return Date.$daysInMonth($pick(monthIndex,d.format("%m").toInt()), $pick(year,d.format("%Y").toInt())) * 86400000;
		},
		year: function(year){
			year = year || new Date().format("%Y").toInt();
			return Date.isLeapYear(year.toInt())?31622400000:31536000000;
		}
	},
	$formats: {
		db: '%Y-%m-%d %H:%M:%S',
		compact: '%Y%m%dT%H%M%S',
		iso8601: '%Y-%m-%dT%H:%M:%S%T',
		rfc822: '%a, %d %b %Y %H:%M:%S %Z',
		'short': '%d %b %H:%M',
		'long': '%B %d, %Y %H:%M'
	},
	
	isLeapYear: function(yr) {
		return new Date(yr,1,29).getDate()==29;
	},

	parseUTC: function(value){
		var localDate = new Date(value);
		var utcSeconds = Date.UTC(localDate.get('year'), localDate.get('mo'),
		localDate.get('date'), localDate.get('hr'), localDate.get('min'), localDate.get('sec'));
		return new Date(utcSeconds);
	},
	
	parse: function(from) {
		var type = $type(from);
		if (type == 'number') return new Date(from);
		if (type != 'string') return from;
		if (!from.length) return null;
		for (var i = 0, j = Date.$parsePatterns.length; i < j; i++) {
			var r = Date.$parsePatterns[i].re.exec(from);
			if (r) {
				try {
					return Date.$parsePatterns[i].handler(r);
				} catch(e) {
					dbug.log('date parse error: ', e);
					return null;
				}
			}
		}
		return new Date(Date.$nativeParse(from));
	},

	parseMonth: function(month, num) {
		var ret = -1;
		switch ($type(month)) {
			case 'object':
				ret = Date.$months[month.get('mo')];
				break;
			case 'number':
				ret = Date.$months[month - 1] || false;
				if (!ret) throw new Error('Invalid month index value must be between 1 and 12:' + index);
				break;
			case 'string':
				var match = Date.$months.filter(function(name) {
					return this.test(name);
				}, new RegExp('^' + month, 'i'));
				if (!match.length) throw new Error('Invalid month string');
				if (match.length > 1) throw new Error('Ambiguous month');
				ret = match[0];
		}
		return (num) ? Date.$months.indexOf(ret) : ret;
	},

	parseDay: function(day, num) {
		var ret = -1;
		switch ($type(day)) {
			case 'number':
				ret = Date.$days[day - 1] || false;
				if (!ret) throw new Error('Invalid day index value must be between 1 and 7');
				break;
			case 'string':
				var match = Date.$days.filter(function(name) {
					return this.test(name);
				}, new RegExp('^' + day, 'i'));
				if (!match.length) throw new Error('Invalid day string');
				if (match.length > 1) throw new Error('Ambiguous day');
				ret = match[0];
		}
		return (num) ? Date.$days.indexOf(ret) : ret;
	},
	
	fixY2K: function(d){
		if (!isNaN(d)) {
			var newDate = new Date(d);
			if (newDate.get('year') < 2000 && d.toString().indexOf(newDate.get('year')) < 0) {
				newDate.increment('year', 100);
			}
			return newDate;
		} else return d;
	},

	$cultures: {
		'US': ['month', 'date', 'year', '/'],
		'GB': ['date', 'month', 'year', '/']
	},

	$culture: 'US',
	
	$language: 'enUS',
	
	$cIndex: function(unit){
		return Date.$cultures[Date.$culture].indexOf(unit)+1;
	},

	$parsePatterns: [
		{
			//"12.31.08", "12-31-08", "12/31/08", "12.31.2008", "12-31-2008", "12/31/2008"
			re: /^(\d{1,2})[\.\-\/](\d{1,2})[\.\-\/](\d{2,4})$/,
			handler: function(bits){
				var d = new Date();
				var culture = Date.$cultures[Date.$culture];
				d.set('year', bits[Date.$cIndex('year')]);
				d.set('month', bits[Date.$cIndex('month')] - 1);
				d.set('date', bits[Date.$cIndex('date')]);
				return Date.fixY2K(d);
			}
		},
		//"12.31.08", "12-31-08", "12/31/08", "12.31.2008", "12-31-2008", "12/31/2008"
		//above plus "10:45pm" ex: 12.31.08 10:45pm
		{
			re: /^(\d{1,2})[\.\-\/](\d{1,2})[\.\-\/](\d{2,4})\s(\d{1,2}):(\d{1,2})(\w{2})$/,
			handler: function(bits){
				var d = new Date();
				d.set('year', bits[Date.$cIndex('year')]);
				d.set('month', bits[Date.$cIndex('month')] - 1);
				d.set('date', bits[Date.$cIndex('date')]);
				d.set('hr', bits[4]);
				d.set('min', bits[5]);
				d.set('ampm', bits[6]);
				return Date.fixY2K(d);
			}
		},
		{
			//"12.31.08 11:59:59", "12-31-08 11:59:59", "12/31/08 11:59:59", "12.31.2008 11:59:59", "12-31-2008 11:59:59", "12/31/2008 11:59:59"
			re: /^(\d{1,2})[\.\-\/](\d{1,2})[\.\-\/](\d{2,4})\s(\d{1,2}):(\d{1,2}):(\d{1,2})/,
			handler: function(bits) {
				var d = new Date();
				var culture = Date.$cultures[Date.$culture];
				d.set('year', bits[Date.$cIndex('year')]);
				d.set('month', bits[Date.$cIndex('month')] - 1);
				d.set('date', bits[Date.$cIndex('date')]);
				d.set('hours', bits[4]);
				d.set('minutes', bits[5]);
				d.set('seconds', bits[6]);
				return Date.fixY2K(d);
			}
		}
	]
});

Number.implement({
	zeroise: function(length) {
		return String(this).zeroise(length);
	}
});

String.implement({
	repeat: function(times) {
		var ret = [];
		for (var i = 0; i < times; i++) ret.push(this);
		return ret.join('');
	},
	zeroise: function(length) {
		return '0'.repeat(length - this.length) + this;
	}

});


/*
Script: Hash.Extras.js
	Extends the Hash native object to include getFromPath which allows a path notation to child elements.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

Hash.implement({
	getFromPath: function(notation) {
		var source = this.getClean();
		notation.replace(/\[([^\]]+)\]|\.([^.[]+)|[^[.]+/g, function(match) {
			if (!source) return;
			var prop = arguments[2] || arguments[1] || arguments[0];
			source = (prop in source) ? source[prop] : null;
			return match;
		});
		return source;
	},
	cleanValues: function(method){
		method = method||$defined;
		this.each(function(v, k){
			if (!method(v)) this.erase(k);
		}, this);
		return this;
	},
	run: function(){
		var args = $arguments;
		this.each(function(v, k){
			if ($type(v) == "function") v.run(args);
		});
	}
});

/*
Script: String.Extras.js
	Extends the String native object to include methods useful in managing various kinds of strings (query strings, urls, html, etc).

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
String.implement({
	stripTags: function() {
		return this.replace(/<\/?[^>]+>/gi, '');
	},
	parseQuery: function(encodeKeys, encodeValues) {
		encodeKeys = $pick(encodeKeys, true);
		encodeValues = $pick(encodeValues, true);
		var vars = this.split(/[&;]/);
		var rs = {};
		if (vars.length) vars.each(function(val) {
			var keys = val.split('=');
			if (keys.length && keys.length == 2) {
				rs[(encodeKeys)?encodeURIComponent(keys[0]):keys[0]] = (encodeValues)?encodeURIComponent(keys[1]):keys[1];
			}
		});
		return rs;
	},
	tidy: function() {
		var txt = this.toString();
		$each({
			"[\xa0\u2002\u2003\u2009]": " ",
			"\xb7": "*",
			"[\u2018\u2019]": "'",
			"[\u201c\u201d]": '"',
			"\u2026": "...",
			"\u2013": "-",
			"\u2014": "--",
			"\uFFFD": "&raquo;"
		}, function(value, key){
			txt = txt.replace(new RegExp(key, 'g'), value);
		});
		return txt;
	},
	cleanQueryString: function(method){
		return this.split("&").filter(method||function(set){
			return $chk(set.split("=")[1]);
		}).join("&");
	},
	findAllEmails: function(){
			return this.match(new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", "gi")) || [];
	}
});


/*
Script: Element.Forms.js
	Extends the Element native object to include methods useful in managing inputs.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
Element.implement({
	tidy: function(){
		this.set('value', this.get('value').tidy());
	},
	getTextInRange: function(start, end) {
		return this.get('value').substring(start, end);
	},
	getSelectedText: function() {
		if(Browser.Engine.trident) return document.selection.createRange().text;
		return this.get('value').substring(this.getSelectionStart(), this.getSelectionEnd());
	},
	getIERanges: function(){
		this.focus();
		var range = document.selection.createRange();
		var re = this.createTextRange();
		var dupe = re.duplicate();
		re.moveToBookmark(range.getBookmark());
		dupe.setEndPoint('EndToStart', re);
		return { start: dupe.text.length, end: dupe.text.length + range.text.length, length: range.text.length, text: range.text };
	},
	getSelectionStart: function() {
		if(Browser.Engine.trident) return this.getIERanges().start;
		return this.selectionStart;
	},
	getSelectionEnd: function() {
		if(Browser.Engine.trident) return this.getIERanges().end;
		return this.selectionEnd;
	},
	getSelectedRange: function() {
		return {
			start: this.getSelectionStart(),
			end: this.getSelectionEnd()
		}
	},
	setCaretPosition: function(pos) {
		if(pos == 'end') pos = this.get('value').length;
		this.selectRange(pos, pos);
		return this;
	},
	getCaretPosition: function() {
		return this.getSelectedRange().start;
	},
	selectRange: function(start, end) {
		this.focus();
		if(Browser.Engine.trident) {
			var range = this.createTextRange();
			range.collapse(true);
			range.moveStart('character', start);
			range.moveEnd('character', end - start);
			range.select();
			return this;
		}
		this.setSelectionRange(start, end);
		return this;
	},
	insertAtCursor: function(value, select) {
		var start = this.getSelectionStart();
		var end = this.getSelectionEnd();
		this.set('value', this.get('value').substring(0, start) + value + this.get('value').substring(end, this.get('value').length));
 		if($pick(select, true)) this.selectRange(start, start + value.length);
		else this.setCaretPosition(start + value.length);
		return this;
	},
	insertAroundCursor: function(options, select) {
		options = $extend({
			before: '',
			defaultMiddle: 'SOMETHING HERE',
			after: ''
		}, options);
		value = this.getSelectedText() || options.defaultMiddle;
		var start = this.getSelectionStart();
		var end = this.getSelectionEnd();
		if(start == end) {
			var text = this.get('value');
			this.set('value', text.substring(0, start) + options.before + value + options.after + text.substring(end, text.length));
			this.selectRange(start + options.before.length, end + options.before.length + value.length);
			text = null;
		} else {
			text = this.get('value').substring(start, end);
			this.set('value', this.get('value').substring(0, start) + options.before + text + options.after + this.get('value').substring(end, this.get('value').length));
			var selStart = start + options.before.length;
			if($pick(select, true)) this.selectRange(selStart, selStart + text.length);
			else this.setCaretPosition(selStart + text.length);
		}	
		return this;
	}
});


Element.Properties.inputValue = {
 
    get: function(){
			 switch(this.get('tag')) {
			 	case 'select':
					vals = this.getSelected().map(function(op){ 
						var v = $pick(op.get('value'),op.get('text')); 
						return (v=="")?op.get('text'):v;
					});
					return this.get('multiple')?vals:vals[0];
				case 'input':
					switch(this.get('type')) {
						case 'checkbox':
							return this.get('checked')?this.get('value'):false;
						case 'radio':
							var checked;
							if (this.get('checked')) return this.get('value');
							$(this.getParent('form')||document.body).getElements('input').each(function(input){
								if (input.get('name') == this.get('name') && input.get('checked')) checked = input.get('value');
							}, this);
							return checked||null;
					}
			 	case 'input': case 'textarea':
					return this.get('value');
				default:
					return this.get('inputValue');
			 }
    },
 
    set: function(value){
			switch(this.get('tag')){
				case 'select':
					this.getElements('option').each(function(op){
						var v = $pick(op.get('value'), op.get('text'));
						if (v=="") v = op.get('text');
						op.set('selected', $splat(value).contains(v));
					});
					break;
				case 'input':
					if (['radio','checkbox'].contains(this.get('type'))) {
						this.set('checked', $type(value)=="boolean"?value:$splat(value).contains(this.get('value')));
						break;
					}
				case 'textarea': case 'input':
					this.set('value', value);
					break;
				default:
					this.set('inputValue', value);
			}
			return this;
    },
		
	erase: function() {
		switch(this.get('tag')) {
			case 'select':
				this.getElements('option').each(function(op) {
					op.erase('selected');
				});
				break;
			case 'input':
				if (['radio','checkbox'].contains(this.get('type'))) {
					this.set('checked', false);
					break;
				}
			case 'input': case 'textarea':
				this.set('value', '');
				break;
			default:
				this.set('inputValue', '');
		}
		return this;
	}

};


/*
Script: Element.Measure.js
	Extends the Element native object to include methods useful in measuring dimensions.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

Element.implement({

	// Daniel Steigerwald - MIT licence
	measure: function(fn) {
		var restore = this.expose();
		fn.apply(this);
		return restore();
	},

	expose: function(){
		var style = this.style;
    var cssText = style.cssText;
    style.visibility = 'hidden';
    style.position = 'absolute';
    if (style.display == 'none') style.display = '';
		return (function(){ return this.set('style', cssText); }).bind(this);
	},
	
	getDimensions: function(options) {
		options = $merge({computeSize: false},options);
		var dim = {};
		function getSize(el, options){
			return (options.computeSize)?el.getComputedSize(options):el.getSize();
		};
		if(this.getStyle('display') == 'none'){
			var restore = this.expose();
			dim = getSize(this, options); //works now, because the display isn't none
			restore(); //put it back where it was
		} else {
			try { //safari sometimes crashes here, so catch it
				dim = getSize(this, options);
			}catch(e){}
		}
		return $chk(dim.x)?$extend(dim, {width: dim.x, height: dim.y}):$extend(dim, {x: dim.width, y: dim.height});
	},
	
	getComputedSize: function(options){
		options = $merge({
			styles: ['padding','border'],
			plains: {height: ['top','bottom'], width: ['left','right']},
			mode: 'both'
		}, options);
		var size = {width: 0,height: 0};
		switch (options.mode){
			case 'vertical':
				delete size.width;
				delete options.plains.width;
				break;
			case 'horizontal':
				delete size.height;
				delete options.plains.height;
				break;
		};
		var getStyles = [];
		//this function might be useful in other places; perhaps it should be outside this function?
		$each(options.plains, function(plain, key){
			plain.each(function(edge){
				options.styles.each(function(style){
					getStyles.push((style=="border")?style+'-'+edge+'-'+'width':style+'-'+edge);
				});
			});
		});
		var styles = this.getStyles.apply(this, getStyles);
		var subtracted = [];
		$each(options.plains, function(plain, key){ //keys: width, height, plains: ['left','right'], ['top','bottom']
			size['total'+key.capitalize()] = 0;
			size['computed'+key.capitalize()] = 0;
			plain.each(function(edge){ //top, left, right, bottom
				size['computed'+edge.capitalize()] = 0;
				getStyles.each(function(style,i){ //padding, border, etc.
					//'padding-left'.test('left') size['totalWidth'] = size['width']+[padding-left]
					if(style.test(edge)) {
						styles[style] = styles[style].toInt(); //styles['padding-left'] = 5;
						if(isNaN(styles[style]))styles[style]=0;
						size['total'+key.capitalize()] = size['total'+key.capitalize()]+styles[style];
						size['computed'+edge.capitalize()] = size['computed'+edge.capitalize()]+styles[style];
					}
					//if width != width (so, padding-left, for instance), then subtract that from the total
					if(style.test(edge) && key!=style && 
						(style.test('border') || style.test('padding')) && !subtracted.contains(style)) {
						subtracted.push(style);
						size['computed'+key.capitalize()] = size['computed'+key.capitalize()]-styles[style];
					}
				});
			});
		});
		if($chk(size.width)) {
			size.width = size.width+this.offsetWidth+size.computedWidth;
			size.totalWidth = size.width + size.totalWidth;
			delete size.computedWidth;
		}
		if($chk(size.height)) {
			size.height = size.height+this.offsetHeight+size.computedHeight;
			size.totalHeight = size.height + size.totalHeight;
			delete size.computedHeight;
		}
		return $extend(styles, size);
	}
});


/*
Script: Element.Pin.js
	Extends the Element native object to include the pin method useful for fixed positioning for elements.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

window.addEvent('domready', function(){
	var test = new Element('div').setStyles({
		position: 'fixed',
		top: 0,
		right: 0
	}).inject(document.body);
	var supported = (test.offsetTop === 0);
	test.dispose();
	Browser.supportsPositionFixed = supported;
});

Element.implement({
	pin: function(enable){
		if(!Browser.loaded) dbug.log('cannot pin ' + this + ' natively because the dom is not ready');
		if (this.getStyle('display') == 'none') {
			dbug.log('cannot pin ' + this + ' because it is hidden');
			return;
		}
		if(enable!==false) {
			var p = this.getPosition();
			if(!this.retrieve('pinned')) {
				var pos = {
					top: (p.y - window.getScroll().y),
					left: (p.x - window.getScroll().x)
				};
				if(Browser.supportsPositionFixed) {
					this.setStyle('position','fixed').setStyles(pos);
				} else {
					this.store('pinnedByJS', true);
					this.setStyles({
						position: 'absolute',
						top: p.y,
						left: p.x
					});
					this.store('scrollFixer', function(){
						if(this.retrieve('pinned')) {
							var to = {
								top: (pos.top.toInt() + window.getScroll().y),
								left: (pos.left.toInt() + window.getScroll().x)
							};
							this.setStyles(to);
						}
					}.bind(this));
					window.addEvent('scroll', this.retrieve('scrollFixer'));
				}
				this.store('pinned', true);
			}
		} else {
			var op;
			if (!Browser.Engine.trident) {
				if (this.getParent().getComputedStyle('position') != 'static') op = this.getParent();
				else op = this.getParent().getOffsetParent();
			}
			var p = this.getPosition(op);
			this.store('pinned', false);
			var reposition;
			if (Browser.supportsPositionFixed && !this.retrieve('pinnedByJS')) {
				reposition = {
					top: (p.y + window.getScroll().y),
					left: (p.x + window.getScroll().x)
				};
			} else {
				this.store('pinnedByJS', false);
				window.removeEvent('scroll', this.retrieve('scrollFixer'));
				reposition = {
					top: (p.y),
					left: (p.x)
				};
			}
			this.setStyles($merge(reposition, {position: 'absolute'}));
		}
		return this.addClass('isPinned');
	},
	unpin: function(){
		return this.pin(false).removeClass('isPinned');
	},
	togglepin: function(){
		this.pin(!this.retrieve('pinned'));
	}
});


/*
Script: Element.Position.js
	Extends the Element native object to include methods useful positioning elements relative to others.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

Element.implement({

	setPosition: function(options){
		$each(options||{}, function(v, k){ if (!$defined(v)) delete options[k]; });
		options = $merge({
			relativeTo: document.body,
			position: {
				x: 'center', //left, center, right
				y: 'center' //top, center, bottom
			},
			edge: false,
			offset: {x:0,y:0},
			returnPos: false,
			relFixedPosition: false,
			ignoreMargins: false,
			allowNegative: false
		}, options);
		//compute the offset of the parent positioned element if this element is in one
		var parentOffset = {x: 0, y: 0};
		var parentPositioned = false;
		var putItBack = this.expose();
    /* dollar around getOffsetParent should not be necessary, but as it does not return 
     * a mootools extended element in IE, an error occurs on the call to expose. See:
		 * http://mootools.lighthouseapp.com/projects/2706/tickets/333-element-getoffsetparent-inconsistency-between-ie-and-other-browsers */
		var offsetParent = $(this.getOffsetParent());
		putItBack();
		if(offsetParent && offsetParent != this.getDocument().body) {
			var putItBack = offsetParent.expose();
			parentOffset = offsetParent.getPosition();
			putItBack();
			parentPositioned = true;
			options.offset.x = options.offset.x - parentOffset.x;
			options.offset.y = options.offset.y - parentOffset.y;
		}
		//upperRight, bottomRight, centerRight, upperLeft, bottomLeft, centerLeft
		//topRight, topLeft, centerTop, centerBottom, center
		function fixValue(option) {
			if($type(option) != "string") return option;
			option = option.toLowerCase();
			var val = {};
			if(option.test('left')) val.x = 'left';
			else if(option.test('right')) val.x = 'right';
			else val.x = 'center';

			if(option.test('upper')||option.test('top')) val.y = 'top';
			else if (option.test('bottom')) val.y = 'bottom';
			else val.y = 'center';
			return val;
		};
		options.edge = fixValue(options.edge);
		options.position = fixValue(options.position);
		if(!options.edge) {
			if(options.position.x == 'center' && options.position.y == 'center') options.edge = {x:'center',y:'center'};
			else options.edge = {x:'left',y:'top'};
		}
		
		this.setStyle('position', 'absolute');
		var rel = $(options.relativeTo) || document.body;
		var top = (rel == document.body)?window.getScroll().y:rel.getPosition().y;
		var left = (rel == document.body)?window.getScroll().x:rel.getPosition().x;
		var dim = this.getDimensions({computeSize: true, styles:['padding', 'border','margin']});
		if (options.ignoreMargins) {
			options.offset.x = options.offset.x - dim['margin-left'];
			options.offset.y = options.offset.y - dim['margin-top'];
		}
		var pos = {};
		var prefY = options.offset.y.toInt();
		var prefX = options.offset.x.toInt();
		switch(options.position.x) {
			case 'left':
				pos.x = left + prefX;
				break;
			case 'right':
				pos.x = left + prefX + rel.offsetWidth;
				break;
			default: //center
				pos.x = left + (((rel == document.body)?window.getSize().x:rel.offsetWidth)/2) + prefX;
				break;
		};
		switch(options.position.y) {
			case 'top':
				pos.y = top + prefY;
				break;
			case 'bottom':
				pos.y = top + prefY + rel.offsetHeight;
				break;
			default: //center
				pos.y = top + (((rel == document.body)?window.getSize().y:rel.offsetHeight)/2) + prefY;
				break;
		};
		
		if(options.edge){
			var edgeOffset = {};
			
			switch(options.edge.x) {
				case 'left':
					edgeOffset.x = 0;
					break;
				case 'right':
					edgeOffset.x = -dim.x-dim.computedRight-dim.computedLeft;
					break;
				default: //center
					edgeOffset.x = -(dim.x/2);
					break;
			};
			switch(options.edge.y) {
				case 'top':
					edgeOffset.y = 0;
					break;
				case 'bottom':
					edgeOffset.y = -dim.y-dim.computedTop-dim.computedBottom;
					break;
				default: //center
					edgeOffset.y = -(dim.y/2);
					break;
			};
			pos.x = pos.x+edgeOffset.x;
			pos.y = pos.y+edgeOffset.y;
		}
		pos = {
			left: ((pos.x >= 0 || parentPositioned || options.allowNegative)?pos.x:0).toInt(),
			top: ((pos.y >= 0 || parentPositioned || options.allowNegative)?pos.y:0).toInt()
		};
		if(rel.getStyle('position') == "fixed"||options.relFixedPosition) {
			pos.top = pos.top.toInt() + window.getScroll().y;
			pos.left = pos.left.toInt() + window.getScroll().x;
		}

		if(options.returnPos) return pos;
		else this.setStyles(pos);
		return this;
	}
});


/*
Script: Element.Shortcuts.js
	Extends the Element native object to include some shortcut methods.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

Element.implement({
	isVisible: function() {
		return this.getStyle('display') != 'none';
	},
	toggle: function() {
		return this[this.isVisible() ? 'hide' : 'show']();
	},
	hide: function() {
		var d;
		try {
			//IE fails here if the element is not in the dom
			if ('none' != this.getStyle('display')) d = this.getStyle('display');
		} catch(e){}
		this.store('originalDisplay', d||'block'); 
		this.setStyle('display','none');
		return this;
	},
	show: function(display) {
		original = this.retrieve('originalDisplay')?this.retrieve('originalDisplay'):this.get('originalDisplay');
		this.setStyle('display',(display || original || 'block'));
		return this;
	},
	swapClass: function(remove, add) {
		return this.removeClass(remove).addClass(add);
	},
	//TODO
	//DO NOT USE THIS METHOD
	//it is temporary, as Mootools 1.1 will negate its requirement
	fxOpacityOk: function(){
		return !Browser.Engine.trident4;
	} 
});


//returns a collection given an id or a selector
$G = function(elements) {
	return $splat($(elements)||$$(elements));
};

/*
Script: Fx.Reveal.js
	Defines Fx.Reveal, a class that shows and hides elements with a transition.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
Fx.Reveal = new Class({
	Extends: Fx.Morph,
	options: {
		styles: ['padding','border','margin'],
		transitionOpacity: true,
		mode:'vertical',
		heightOverride: null,
		widthOverride: null
/*	onShow: $empty,
		onHide: $empty */
	},
	dissolve: function(){
		try {
			if(!this.hiding && !this.showing) {
				if(this.element.getStyle('display') != 'none'){
					this.hiding = true;
					this.showing = false;
					this.hidden = true;
					var startStyles = this.element.getComputedSize({
						styles: this.options.styles,
						mode: this.options.mode
					});
					var setToAuto = this.element.style.height === ""||this.element.style.height=="auto";
					this.element.setStyle('display', 'block');
					if (this.element.fxOpacityOk() && this.options.transitionOpacity) startStyles.opacity = 1;
					var zero = {};
					$each(startStyles, function(style, name){
						zero[name] = [style, 0]; 
					}, this);
					var overflowBefore = this.element.getStyle('overflow');
					this.element.setStyle('overflow', 'hidden');
					//put the final fx method at the front of the chain
					this.$chain = this.$chain || [];
					this.$chain.unshift(function(){
						if(this.hidden) {
							this.hiding = false;
							$each(startStyles, function(style, name) {
								startStyles[name] = style;
							}, this);
							this.element.setStyles($merge({display: 'none', overflow: overflowBefore}, startStyles));
							if (setToAuto) this.element.setStyle('height', 'auto');
						}
						this.fireEvent('onHide', this.element);
						this.callChain();
					}.bind(this));
					this.start(zero);
				} else {
					this.callChain.delay(10, this);
					this.fireEvent('onComplete', this.element);
					this.fireEvent('onHide', this.element);
				}
			} else if (this.options.link == "chain") {
				this.chain(this.dissolve.bind(this));
			} else if (this.options.link == "cancel" && !this.hiding) {
				this.cancel();
				this.dissolve();
			}
		} catch(e) {
			this.hiding = false;
			this.element.hide();
			this.callChain.delay(10, this);
			this.fireEvent('onComplete', this.element);
			this.fireEvent('onHide', this.element);
		}
		return this;
	},
	reveal: function(){
		try {
			if(!this.showing && !this.hiding) {
				if(this.element.getStyle('display') == "none" || 
					 this.element.getStyle('visiblity') == "hidden" || 
					 this.element.getStyle('opacity')==0){
					this.showing = true;
					this.hiding = false;
					this.hidden = false;
					//toggle display, but hide it
					var before = this.element.getStyles('visibility', 'display', 'position');
					this.element.setStyles({
						visibility: 'hidden',
						display: 'block',
						position:'absolute'
					});
					var setToAuto = this.element.style.height === ""||this.element.style.height=="auto";
					//enable opacity effects
					if(this.element.fxOpacityOk() && this.options.transitionOpacity) this.element.setStyle('opacity',0);
					//create the styles for the opened/visible state
					var startStyles = this.element.getComputedSize({
						styles: this.options.styles,
						mode: this.options.mode
					});
					//reset the styles back to hidden now
					this.element.setStyles(before);
					$each(startStyles, function(style, name) {
						startStyles[name] = style;
					}, this);
					//if we're overridding height/width
					if($chk(this.options.heightOverride)) startStyles['height'] = this.options.heightOverride.toInt();
					if($chk(this.options.widthOverride)) startStyles['width'] = this.options.widthOverride.toInt();
					if(this.element.fxOpacityOk() && this.options.transitionOpacity) startStyles.opacity = 1;
					//create the zero state for the beginning of the transition
					var zero = { 
						height: 0,
						display: 'block'
					};
					$each(startStyles, function(style, name){ zero[name] = 0 }, this);
					var overflowBefore = this.element.getStyle('overflow');
					//set to zero
					this.element.setStyles($merge(zero, {overflow: 'hidden'}));
					//start the effect
					this.start(startStyles);
					if (!this.$chain) this.$chain = [];
					this.$chain.unshift(function(){
						if (!this.options.heightOverride && setToAuto) {
							if (["vertical", "both"].contains(this.options.mode)) this.element.setStyle('height', 'auto');
							if (["width", "both"].contains(this.options.mode)) this.element.setStyle('width', 'auto');
						}
						if(!this.hidden) this.showing = false;
						this.element.setStyle('overflow', overflowBefore);
						this.callChain();
						this.fireEvent('onShow', this.element);
					}.bind(this));
				} else {
					this.callChain();
					this.fireEvent('onComplete', this.element);
					this.fireEvent('onShow', this.element);
				}
			} else if (this.options.link == "chain") {
				this.chain(this.reveal.bind(this));
			} else if (this.options.link == "cancel" && !this.showing) {
				this.cancel();
				this.reveal();
			}
		} catch(e) {
			this.element.setStyles({
				display: 'block',
				visiblity: 'visible',
				opacity: 1
			});
			this.showing = false;
			this.callChain.delay(10, this);
			this.fireEvent('onComplete', this.element);
			this.fireEvent('onShow', this.element);
		}
		return this;
	},
	toggle: function(){
		try {
			if(this.element.getStyle('display') == "none" || 
				 this.element.getStyle('visiblity') == "hidden" || 
				 this.element.getStyle('opacity')==0){
				this.reveal();
		 	} else {
				this.dissolve();
			}
		} catch(e) { this.show(); }
	 return this;
	}
});

Element.Properties.reveal = {

	set: function(options){
		var reveal = this.retrieve('reveal');
		if (reveal) reveal.cancel();
		return this.eliminate('reveal').store('reveal:options', $extend({link: 'cancel'}, options));
	},

	get: function(options){
		if (options || !this.retrieve('reveal')){
			if (options || !this.retrieve('reveal:options')) this.set('reveal', options);
			this.store('reveal', new Fx.Reveal(this, this.retrieve('reveal:options')));
		}
		return this.retrieve('reveal');
	}

};

Element.Properties.dissolve = Element.Properties.reveal;

Element.implement({

	reveal: function(options){
		this.get('reveal', options).reveal();
		return this;
	},
	
	dissolve: function(options){
		this.get('reveal', options).dissolve();
		return this;
	},

	nix: function() {
		var  params = Array.link(arguments, {destroy: Boolean.type, options: Object.type});
		this.get('reveal', params.options).dissolve().chain(function(){
			this[params.destroy?'destroy':'dispose']();
		}.bind(this));
		return this;
	}
});


/*
Script: modalizer.js
	Defines Modalizer: functionality to overlay the window contents with a semi-transparent layer that prevents interaction with page content until it is removed

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var Modalizer = new Class({
	defaultModalStyle: {
		display:'block',
		position:'fixed',
		top:0,
		left:0,	
		'z-index':5000,
		'background-color':'#333',
		opacity:0.8
	},
	setModalOptions: function(options){
		this.modalOptions = $merge({
			width:(window.getScrollSize().x+300),
			height:(window.getScrollSize().y+300),
			elementsToHide: 'select',
			hideOnClick: true,
			modalStyle: {},
			updateOnResize: true,
			layerId: 'modalOverlay',
			onModalHide: $empty,
			onModalShow: $empty
		}, this.modalOptions, options);
		return this;
	},
	layer: function(){
		if (!this.modalOptions.layerId) this.setModalOptions();
		return $(this.modalOptions.layerId) || new Element('div', {id: this.modalOptions.layerId}).inject(document.body);
	},
	resize: function(){
		if(this.layer()) {
			this.layer().setStyles({
				width:(window.getScrollSize().x+300),
				height:(window.getScrollSize().y+300)
			});
		}
	},
	setModalStyle: function (styleObject){
		this.modalOptions.modalStyle = styleObject;
		this.modalStyle = $merge(this.defaultModalStyle, {
			width:this.modalOptions.width,
			height:this.modalOptions.height
		}, styleObject);
		if(this.layer()) this.layer().setStyles(this.modalStyle);
		return(this.modalStyle);
	},
	modalShow: function(options){
		this.setModalOptions(options);
		this.layer().setStyles(this.setModalStyle(this.modalOptions.modalStyle));
		if(Browser.Engine.trident4) this.layer().setStyle('position','absolute');
		this.layer().removeEvents('click').addEvent('click', function(){
			this.modalHide(this.modalOptions.hideOnClick);
		}.bind(this));
		this.bound = this.bound||{};
		if(!this.bound.resize && this.modalOptions.updateOnResize) {
			this.bound.resize = this.resize.bind(this);
			window.addEvent('resize', this.bound.resize);
		}
		if ($type(this.modalOptions.onModalShow)  == "function") this.modalOptions.onModalShow();
		this.togglePopThroughElements(0);
		this.layer().setStyle('display','block');
		return this;
	},
	modalHide: function(override, force){
		if(override === false) return false; //this is internal, you don't need to pass in an argument
		this.togglePopThroughElements(1);
		if ($type(this.modalOptions.onModalHide) == "function") this.modalOptions.onModalHide();
		this.layer().setStyle('display','none');
		if(this.modalOptions.updateOnResize) {
			this.bound = this.bound||{};
			if(!this.bound.resize) this.bound.resize = this.resize.bind(this);
			window.removeEvent('resize', this.bound.resize);
		}
		return this;
	},
	togglePopThroughElements: function(opacity){
		if(Browser.Engine.trident4 || (Browser.Engine.gecko && Browser.Platform.mac)) {
			$$(this.modalOptions.elementsToHide).each(function(sel){
				sel.setStyle('opacity', opacity);
			});
		}
	}
});

/*
Script: StyleWriter.js

Provides a simple method for injecting a css style element into the DOM if it's not already present.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

var StyleWriter = new Class({
	createStyle: function(css, id) {
		window.addEvent('domready', function(){
			try {
				if($(id) && id) return;
				var style = new Element('style', {id: id||''}).inject($$('head')[0]);
				if (Browser.Engine.trident) style.styleSheet.cssText = css;
				else style.set('text', css);
			}catch(e){dbug.log('error: %s',e);}
		}.bind(this));
	}
});

/*
Script: StickyWin.js

Creates a div within the page with the specified contents at the location relative to the element you specify; basically an in-page popup maker.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

var StickyWin = new Class({
	Implements: [Options, Events, StyleWriter, ToElement],
	options: {
//		onDisplay: $empty,
//		onClose: $empty,
		closeClassName: 'closeSticky',
		pinClassName: 'pinSticky',
		content: '',
		zIndex: 10000,
		className: '',
//		id: ... set above in initialize function
/*  	these are the defaults for setPosition anyway
		************************************************
		edge: false, //see Element.setPosition
		position: 'center', //center, corner == upperLeft, upperRight, bottomLeft, bottomRight
		offset: {x:0,y:0},
		relativeTo: document.body, */
		width: false,
		height: false,
		timeout: -1,
		allowMultipleByClass: false,
		allowMultiple: true,
		showNow: true,
		useIframeShim: true,
		iframeShimSelector: ''
	},
	css: '.SWclearfix:after {content: "."; display: block; height: 0; clear: both; visibility: hidden;}'+
			 '.SWclearfix {display: inline-table;}'+
			 '* html .SWclearfix {height: 1%;}'+
			 '.SWclearfix {display: block;}',
	initialize: function(options){
		this.options.inject = {
			target: document.body,
			where: 'bottom' 
		};
		this.setOptions(options);
		
		this.id = this.options.id || 'StickyWin_'+new Date().getTime();
		this.makeWindow();

		if(this.options.content) this.setContent(this.options.content);
		if(this.options.timeout > 0) {
			this.addEvent('onDisplay', function(){
				this.hide.delay(this.options.timeout, this)
			}.bind(this));
		}
		if(this.options.showNow) this.show();
		//add css for clearfix
		this.createStyle(this.css, 'StickyWinClearFix');
	},
	makeWindow: function(){
		this.destroyOthers();
		if(!$(this.id)) {
			this.win = new Element('div', {
				id:		this.id
			}).addClass(this.options.className).addClass('StickyWinInstance').addClass('SWclearfix').setStyles({
			 	display:'none',
				position:'absolute',
				zIndex:this.options.zIndex
			}).inject(this.options.inject.target, this.options.inject.where).store('StickyWin', this);			
		} else this.win = $(this.id);
		this.element = this.win;
		if(this.options.width && $type(this.options.width.toInt())=="number") this.win.setStyle('width', this.options.width.toInt());
		if(this.options.height && $type(this.options.height.toInt())=="number") this.win.setStyle('height', this.options.height.toInt());
		return this;
	},
	show: function(suppressEvent){
		this.showWin();
		if (!suppressEvent) this.fireEvent('onDisplay');
		if(this.options.useIframeShim) this.showIframeShim();
		this.visible = true;
		return this;
	},
	showWin: function(){
		if(!this.positioned) this.position();
		this.win.show();
	},
	hide: function(suppressEvent){
		if(!suppressEvent) this.fireEvent('onClose');
		this.hideWin();
		if(this.options.useIframeShim) this.hideIframeShim();
		this.visible = false;
		return this;
	},
	hideWin: function(){
		this.win.setStyle('display','none');
	},
	destroyOthers: function() {
		if(!this.options.allowMultipleByClass || !this.options.allowMultiple) {
			$$('div.StickyWinInstance').each(function(sw) {
				if(!this.options.allowMultiple || (!this.options.allowMultipleByClass && sw.hasClass(this.options.className))) 
					sw.retrieve('StickyWin').destroy();
			}, this);
		}
	},
	setContent: function(html) {
		if(this.win.getChildren().length>0) this.win.empty();
		if($type(html) == "string") this.win.set('html', html);
		else if ($(html)) this.win.adopt(html);
		this.win.getElements('.'+this.options.closeClassName).each(function(el){
			el.addEvent('click', this.hide.bind(this));
		}, this);
		this.win.getElements('.'+this.options.pinClassName).each(function(el){
			el.addEvent('click', this.togglepin.bind(this));
		}, this);
		return this;
	},	
	position: function(options){
		this.positioned = true;
		this.setOptions(options);
		this.win.setPosition({
			allowNegative: true,
			relativeTo: this.options.relativeTo,
			position: this.options.position,
			offset: this.options.offset,
			edge: this.options.edge
		});
		if(this.shim) this.shim.position();
		return this;
	},
	pin: function(pin) {
		if(!this.win.pin) {
			dbug.log('you must include element.pin.js!');
			return this;
		}
		this.pinned = $pick(pin, true);
		this.win.pin(pin);
		return this;
	},
	unpin: function(){
		return this.pin(false);
	},
	togglepin: function(){
		return this.pin(!this.pinned);
	},
	makeIframeShim: function(){
		if(!this.shim){
			var el = (this.options.iframeShimSelector)?this.win.getElement(this.options.iframeShimSelector):this.win;
			this.shim = new IframeShim(el, {
				display: false,
				name: 'StickyWinShim'
			});
		}
	},
	showIframeShim: function(){
		if(this.options.useIframeShim) {
			this.makeIframeShim();
			this.shim.show();
		}
	},
	hideIframeShim: function(){
		if(this.shim) this.shim.hide();
	},
	destroy: function(){
		if (this.win) this.win.dispose();
		if(this.options.useIframeShim && this.shim) this.shim.dispose();
		if($('modalOverlay'))$('modalOverlay').dispose();
	}
});


/*
Script: StickyWin.Fx.js

Extends StickyWin to create popups that fade in and out and can be dragged and resized (requires StickyWin.Fx.Drag.js).

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
StickyWin.Fx = new Class({
	Extends: StickyWin,
	options: {
		fade: true,
		fadeDuration: 150,
//	fadeTransition: 'sine:in:out',
		draggable: false,
		dragOptions: {},
		dragHandleSelector: '.dragHandle',
		resizable: false,
		resizeOptions: {},
		resizeHandleSelector: ''
	},
	setContent: function(html){
		this.parent(html);
		if(this.options.draggable) this.makeDraggable();
		if(this.options.resizable) this.makeResizable();
		return this;
	},	
	hideWin: function(){
		if(this.options.fade) this.fade(0);
		else this.parent();
	},
	showWin: function(){
		if(this.options.fade) this.fade(1);
		else this.parent();
	},
	hide: function(){
		this.parent(this.options.fade);
	},
	show: function(){
		this.parent(this.options.fade);
	},
	fade: function(to){
		if(!this.fadeFx) {
			this.win.setStyles({
				opacity: 0,
				display: 'block'
			});
			var opts = {
				property: 'opacity',
				duration: this.options.fadeDuration
			};
			if (this.options.fadeTransition) opts.transition = this.options.fadeTransition;
			this.fadeFx = new Fx.Tween(this.win, opts);
		}
		if (to > 0) {
			this.win.setStyle('display','block');
			this.position();
		}
		this.fadeFx.clearChain();
		this.fadeFx.start(to).chain(function (){
			if(to == 0) {
				this.win.setStyle('display', 'none');
				this.fireEvent('onClose');
			} else {
				this.fireEvent('onDisplay');
			}
		}.bind(this));
		return this;
	},
	makeDraggable: function(){
		dbug.log('you must include Drag.js, cannot make draggable');
	},
	makeResizable: function(){
		dbug.log('you must include Drag.js, cannot make resizable');
	}
});
//legacy
var StickyWinFx = StickyWin.Fx;

/*
Script: StickyWin.Fx.Drag.js

Implements drag and resize functionaity into StickyWin.Fx. See StickyWin.Fx for the options.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/

if(typeof Drag != "undefined"){
	StickyWin.Fx.implement({
		makeDraggable: function(){
			var toggled = this.toggleVisible(true);
			if(this.options.useIframeShim) {
				this.makeIframeShim();
				var onComplete = (this.options.dragOptions.onComplete || $empty);
				this.options.dragOptions.onComplete = function(){
					onComplete();
					this.shim.position();
				}.bind(this);
			}
			if(this.options.dragHandleSelector) {
				var handle = this.win.getElement(this.options.dragHandleSelector);
				if (handle) {
					handle.setStyle('cursor','move');
					this.options.dragOptions.handle = handle;
				}
			}
			this.win.makeDraggable(this.options.dragOptions);
			if (toggled) this.toggleVisible(false);
		}, 
		makeResizable: function(){
			var toggled = this.toggleVisible(true);
			if(this.options.useIframeShim) {
				this.makeIframeShim();
				var onComplete = (this.options.resizeOptions.onComplete || $empty);
				this.options.resizeOptions.onComplete = function(){
					onComplete();
					this.shim.position();
				}.bind(this);
			}
			if(this.options.resizeHandleSelector) {
				var handle = this.win.getElement(this.options.resizeHandleSelector);
				if(handle) this.options.resizeOptions.handle = this.win.getElement(this.options.resizeHandleSelector);
			}
			this.win.makeResizable(this.options.resizeOptions);
			if (toggled) this.toggleVisible(false);
		},
		toggleVisible: function(show){
			if(!this.visible && Browser.Engine.webkit && $pick(show, true)) {
				this.win.setStyles({
					display: 'block',
					opacity: 0
				});
				return true;
			} else if(!$pick(show, false)){
				this.win.setStyles({
					display: 'none',
					opacity: 1
				});
				return false;
			}
			return false;
		}
	});
}


/*
Script: StickyWin.Modal.js

This script extends StickyWin and StickyWin.Fx classes to add Modalizer functionality.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
(function(){
var modalWinBase = function(extend){
	return {
		Extends: extend,
		initialize: function(options){
			options = options||{};
			this.setModalOptions($merge(options.modalOptions||{}, {
				onModalHide: function(){
						this.hide(false);
					}.bind(this)
				}));
			this.parent(options);
		},
		show: function(showModal){
			if($pick(showModal, true)) {
				this.modalShow();
				if (this.modalOptions.elementsToHide) this.win.getElements(this.modalOptions.elementsToHide).setStyle('opacity', 1);
			}
			this.parent();
		},
		hide: function(hideModal){
			if($pick(hideModal, true)) this.modalHide();
			this.parent();
		}
	}
};

StickyWin.Modal = new Class(modalWinBase(StickyWin));
StickyWin.Modal.implement(new Modalizer());
if (StickyWin.Fx) StickyWin.Fx.Modal = new Class(modalWinBase(StickyWin.Fx));
try { StickyWin.Fx.Modal.implement(new Modalizer()); }catch(e){}
})();
//legacy
var StickyWinModal = StickyWin.Modal;
if (StickyWin.Fx) var StickyWinFxModal = StickyWin.Fx.Modal;

/*
Script: StickyWin.Ajaxjs

Adds ajax functionality to all the StickyWin classes.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
(function(){
	var SWA = function(extend){
		return {
			Extends: extend,
			options: {
				url: '',
				showNow: false,
				requestOptions: {
					method: 'get'
				},
				wrapWithUi: false, 
				caption: '',
				uiOptions:{},
				handleResponse: function(response){
					var responseScript = "";
					this.Request.response.text.stripScripts(function(script){	responseScript += script; });
					if(this.options.wrapWithUi) response = StickyWin.ui(this.options.caption, response, this.options.uiOptions);
					this.setContent(response);
					this.show();
					if (this.evalScripts) $exec(responseScript);
				}
			},
			initialize: function(options){
				this.parent(options);
				this.evalScripts = this.options.requestOptions.evalScripts;
				this.options.requestOptions.evalScripts = false;
				this.createRequest();
			},
			createRequest: function(){
				this.Request = new Request(this.options.requestOptions).addEvent('onSuccess',
					this.options.handleResponse.bind(this));
			},
			update: function(url, options){
				this.Request.setOptions(options).send({url: url||this.options.url});
				return this;
			}
		};
	};
	try {	StickyWin.Ajax = new Class(SWA(StickyWin)); } catch(e){}
	try {	StickyWin.Fx.Ajax = new Class(SWA(StickyWin.Fx)); } catch(e){}
	try {	StickyWin.Modal.Ajax = new Class(SWA(StickyWin.Modal)); } catch(e){}
	try {	StickyWin.Fx.Modal.Ajax = new Class(SWA(StickyWin.Fx.Modal)); } catch(e){}
})();
//legacy
if (window.StickyWinModal) StickyWinModal.Ajax = StickyWin.Modal.Ajax;
if (StickyWin.Fx) {
	StickyWinFx.Ajax = StickyWin.Fx.Ajax;
	StickyWinFxModal.Ajax = StickyWin.Fx.Modal.Ajax;
}


/*
Script: StickyWin.alert.js
	Defines StickyWin.alert, a simple little alert box with a close button.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
StickyWin.alert = function(msghdr, msg, baseHref) {
	baseHref = baseHref||"http://www.cnet.com/html/rb/assets/global/simple.error.popup";
	msg = '<p class="errorMsg SWclearfix" style="margin: 0px;min-height:10px">' +
						'<img src="'+baseHref+'/icon_problems_sm.gif"'+
						' class="bang clearfix" style="float: left; width: 30px; height: 30px; margin: 3px 5px 5px 0px;">'
						 + msg + '</p>';
	var body = StickyWin.ui(msghdr, msg, {width: 250});
	return new StickyWin.Modal({
		modalOptions: {
			modalStyle: {
				zIndex: 11000
			}
		},
		zIndex: 110001,
		content: body,
		position: 'center' //center, corner
	});
};

/*
Script: StickyWin.ui.js

Creates an html holder for in-page popups using a default style.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
StickyWin.UI = new Class({
	Implements: [Options, ToElement, StyleWriter],
	options: {
		width: 300,
		css: "div.DefaultStickyWin div.body{font-family:verdana; font-size:11px; line-height: 13px;}"+
			"div.DefaultStickyWin div.top_ul{background:url({%baseHref%}full.png) top left no-repeat; height:30px; width:15px; float:left}"+
			"div.DefaultStickyWin div.top_ur{position:relative; left:0px !important; left:-4px; background:url({%baseHref%}full.png) top right !important; height:30px; margin:0px 0px 0px 15px !important; margin-right:-4px; padding:0px}"+
			"div.DefaultStickyWin h1.caption{clear: none !important; margin:0px 5px 0px 0px !important; overflow: hidden; padding:0 !important; font-weight:bold; color:#555; font-size:14px !important; position:relative; top:8px !important; left:5px !important; float: left; height: 22px !important;}"+
			"div.DefaultStickyWin div.middle, div.DefaultStickyWin div.closeBody {background:url({%baseHref%}body.png) top left repeat-y; margin:0px 20px 0px 0px !important;	margin-bottom: -3px; position: relative;	top: 0px !important; top: -3px;}"+
			"div.DefaultStickyWin div.body{background:url({%baseHref%}body.png) top right repeat-y; padding:8px 30px 8px 0px !important; margin-left:5px !important; position:relative; right:-20px !important;}"+
			"div.DefaultStickyWin div.bottom{clear:both}"+
			"div.DefaultStickyWin div.bottom_ll{background:url({%baseHref%}full.png) bottom left no-repeat; width:15px; height:15px; float:left}"+
			"div.DefaultStickyWin div.bottom_lr{background:url({%baseHref%}full.png) bottom right; position:relative; left:0px !important; left:-4px; margin:0px 0px 0px 15px !important; margin-right:-4px; height:15px}"+
			"div.DefaultStickyWin div.closeButtons{text-align: center; background:url({%baseHref%}body.png) top right repeat-y; padding: 0px 30px 8px 0px; margin-left:5px; position:relative; right:-20px}"+
			"div.DefaultStickyWin a.button:hover{background:url({%baseHref%}big_button_over.gif) repeat-x}"+
			"div.DefaultStickyWin a.button {background:url({%baseHref%}big_button.gif) repeat-x; margin: 2px 8px 2px 8px; padding: 2px 12px; cursor:pointer; border: 1px solid #999 !important; text-decoration:none; color: #000 !important;}"+
			"div.DefaultStickyWin div.closeButton{width:13px; height:13px; background:url({%baseHref%}closebtn.gif) no-repeat; position: absolute; right: 0px; margin:10px 15px 0px 0px !important; cursor:pointer;top:0px}"+
			"div.DefaultStickyWin div.dragHandle {	width: 11px;	height: 25px;	position: relative;	top: 5px;	left: -3px;	cursor: move;	background: url({%baseHref%}drag_corner.gif); float: left;}",
		cornerHandle: false,
		cssClass: '',
		baseHref: 'http://www.cnet.com/html/rb/assets/global/stickyWinHTML/',
		buttons: [],
		cssId: 'defaultStickyWinStyle',
		cssClassName: 'DefaultStickyWin',
		closeButton: true
/*	These options are deprecated:		
		closeTxt: false,
		onClose: $empty,
		confirmTxt: false,
		onConfirm: $empty	*/
	},
	initialize: function() {
		var args = this.getArgs(arguments);
		this.setOptions(args.options);
		this.legacy();
		var css = this.options.css.substitute({baseHref: this.options.baseHref}, /\\?\{%([^}]+)%\}/g);
		if (Browser.Engine.trident4) css = css.replace(/png/g, 'gif');
		this.createStyle(css, this.options.cssId);
		this.build();
		this.setContent(args.caption, args.body);
	},
	getArgs: function(){
		return StickyWin.UI.getArgs.apply(this, arguments);
	},
	legacy: function(){
		var opt = this.options; //saving bytes
		//legacy support
		if(opt.confirmTxt) opt.buttons.push({text: opt.confirmTxt, onClick: opt.onConfirm || $empty});
		if(opt.closeTxt) opt.buttons.push({text: opt.closeTxt, onClick: opt.onClose || $empty});
	},
	build: function(){
		var opt = this.options;

		var container = new Element('div', {
			'class': opt.cssClassName
		});
		if (opt.width) container.setStyle('width', opt.width);
		this.element = container;
		this.element.store('StickyWinUI', this);
		if(opt.cssClass) container.addClass(opt.cssClass);
		

		var bodyDiv = new Element('div').addClass('body');
		this.body = bodyDiv;
		
		var top_ur = new Element('div').addClass('top_ur');
		this.top_ur = top_ur;
		this.top = new Element('div').addClass('top').adopt(
				new Element('div').addClass('top_ul')
			).adopt(top_ur);
		container.adopt(this.top);
		
		if(opt.cornerHandle) new Element('div').addClass('dragHandle').inject(top_ur, 'top');
		
		//body
		container.adopt(new Element('div').addClass('middle').adopt(bodyDiv));
		//close buttons
		if(opt.buttons.length > 0){
			var closeButtons = new Element('div').addClass('closeButtons');
			opt.buttons.each(function(button){
				if(button.properties && button.properties.className){
					button.properties['class'] = button.properties.className;
					delete button.properties.className;
				}
				var properties = $merge({'class': 'closeSticky'}, button.properties);
				new Element('a').addEvent('click',
					button.onClick || $empty).appendText(
					button.text).inject(closeButtons).setProperties(properties).addClass('button');
			});
			container.adopt(new Element('div').addClass('closeBody').adopt(closeButtons));
		}
		//footer
		container.adopt(
			new Element('div').addClass('bottom').adopt(
					new Element('div').addClass('bottom_ll')
				).adopt(
					new Element('div').addClass('bottom_lr')
			)
		);
		if (this.options.closeButton) container.adopt(new Element('div').addClass('closeButton').addClass('closeSticky'));
		return this;
	},
	makeCaption: function(caption) {
		if (!caption) return this.destroyCaption();
		this.caption = caption;
		var opt = this.options;
		var h1Caption = new Element('h1').addClass('caption');
		if (opt.width) h1Caption.setStyle('width', (opt.width-(opt.cornerHandle?70:60)));
		if($(this.caption)) h1Caption.adopt(this.caption);
		else h1Caption.set('html', this.caption);
		this.top_ur.adopt(h1Caption);
		this.h1 = h1Caption;
		if(!this.options.cornerHandle) this.h1.addClass('dragHandle');
		return this;
	},
	destroyCaption: function(){
		if (this.h1) {
			this.h1.destroy();
			this.h1 = null;
		}
		return this;
	},
	setContent: function(){
		var args = this.getArgs.apply(this, arguments);
		var caption = args.caption;
		var body = args.body;
		if (this.h1) this.destroyCaption();
		this.makeCaption(caption);
		if ($(body)) this.body.empty().adopt(body);
		else this.body.set('html', body);	
		return this;
	}
});
StickyWin.UI.getArgs = function(){
	var input = $type(arguments[0]) == "arguments"?arguments[0]:arguments;
	var cap = input[0], bod = input[1];
	var args = Array.link(input, {options: Object.type});
	if (input.length == 3 || (!args.options && input.length == 2)) {
		args.caption = cap;
		args.body = bod;
	} else if (($type(bod) == 'object' || !bod) && cap && $type(cap) != 'object'){
		args.body = cap;
	}
	return args;
};

StickyWin.ui = function(caption, body, options){
	return $(new StickyWin.UI(caption, body, options))
};


/*
Script: StickyWin.ui.pointy.js

Creates an html holder for in-page popups using a default style - this one including a pointer in the specified direction.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
StickyWin.UI.Pointy = new Class({
	Extends: StickyWin.UI,
	options: {
		theme: 'dark',
		themes: {
			dark: {
				bgColor: '#333',
				fgColor: '#ddd',
				imgset: 'dark'
			},
			light: {
				bgColor: '#ccc',
				fgColor: '#333',
				imgset: 'light'
			}
		},
		css: "div.DefaultPointyTip {position: relative}"+
		"div.DefaultPointyTip div.body{background: {%bgColor%}; color: {%fgColor%}; right: 0px !important;padding:  0px 10px !important;margin-left: 0px !important;font-family: verdana;font-size: 11px;line-height: 13px;position: relative;}"+
		"div.DefaultPointyTip div.top {position: relative;height: 25px; overflow: visible}"+
		"div.DefaultPointyTip div.top_ul{background: url({%baseHref%}{%imgset%}_back.png) top left no-repeat;width: 8px;height: 25px; position: absolute; left: 0px;}"+
		"div.DefaultPointyTip div.top_ur{background: url({%baseHref%}{%imgset%}_back.png) top right !important;margin: 0 0 0 8px !important;height: 25px;position: relative;left: 0px !important;padding: 0;}"+
		"div.DefaultPointyTip h1.caption{color: {%fgColor%};left: 0px !important;top: 4px !important;clear: none !important;overflow: hidden;font-weight: 700;font-size: 12px !important;position: relative;float: left;height: 22px !important;margin: 0 22px 0 0 !important;padding: 0 !important;}"+
		"div.DefaultPointyTip div.middle,div.DefaultPointyTip div.closeBody{background:  {%bgColor%};margin: 0 0px 0 0 !important;position: relative;top: 0 !important;}"+
		"div.DefaultPointyTip div.bottom {clear: both;} "+
		"div.DefaultPointyTip div.bottom_ll{font-size:1; background: url({%baseHref%}{%imgset%}_back.png) bottom left no-repeat;width: 6px;height: 6px;position: absolute; left: 0px;}"+
		"div.DefaultPointyTip div.bottom_lr{font-size:1; background: url({%baseHref%}{%imgset%}_back.png) bottom right;height: 6px;margin: 0 0 0 6px !important;position: relative;left: 0 !important;}"+
		"div.DefaultPointyTip div.noCaption{height: 6px; overflow: hidden}"+
		"div.DefaultPointyTip div.closeButton{width:13px; height:13px; background:url({%baseHref%}{%imgset%}_x.png) no-repeat; position: absolute; right: 0px; margin:4px 0px 0px !important; cursor:pointer; z-index: 1; top: 0px;}",
		baseHref: 'http://cnetjavascript.googlecode.com/svn/trunk/Assets/PointyTip/',
		divot: '{%baseHref%}{%imgset%}_divot.png',
		divotSize: 22,
		direction: 12,
		cssId: 'defaultPointyTipStyle',
		cssClassName: 'DefaultPointyTip'
	},
	initialize: function() {
		var args = this.getArgs(arguments);
		this.setOptions(args.options);
		$extend(this.options, this.options.themes[this.options.theme]);
		this.options.css = this.options.css.substitute(this.options, /\\?\{%([^}]+)%\}/g);
		if (args.options && args.options.theme) {
			while (!this.id) {
				var id = $random(0, 999999999);
				if (!StickyWin.UI.Pointy[id]) {
					StickyWin.UI.Pointy[id] = this;
					this.id = id;
				}
			}
			this.options.css = this.options.css.replace(/div\.DefaultPointyTip/g, "div#pointy_"+this.id);
			this.options.cssId = "pointyTipStyle_" + this.id;
		}
		if ($type(this.options.direction) == 'string') {
			var map = {
				left: 9,
				right: 3,
				up: 12,
				down: 6
			};
			this.options.direction = map[this.options.direction];
		}
		
		this.options.divot = this.options.divot.substitute(this.options, /\\?\{%([^}]+)%\}/g);
		if (Browser.Engine.trident4) this.options.divot = this.options.divot.replace(/png/g, 'gif');
		
		this.parent(args.caption, args.body, this.options);
		if (this.id) $(this).set('id', "pointy_"+this.id);
	},
	build: function(){
		this.parent();
		var opt = this.options;
		this.pointyWrapper = new Element('div', {
			'class': 'pointyWrapper'
		}).inject($(this));
		$(this).getChildren().each(function(el){
			if (el != this.pointyWrapper) this.pointyWrapper.grab(el);
		}, this);

		var w = opt.divotSize;
		var h = w;
		var left = (opt.width - opt.divotSize)/2;
		var orient = function(){
			switch(opt.direction) {
				case 12: case 1: case 11:
					return {
						height: h/2
					};
				case 5: case 6: case 7:
					return {
						height: h/2,
						backgroundPosition: '0 -'+h/2+'px'
					};
				case 8: case 9: case 10:
					return {
						width: w/2
					};
				case 2: case 3: case 4:
					return {
						width: w/2,
						backgroundPosition: '100%'
					};
			};
		};
		this.pointer = new Element('div', {
			styles: $extend({
				background: "url("+opt.divot+") no-repeat",
				width: w,
				height: h,
				overflow: 'hidden'
			}, orient()),
			'class': 'pointyDivot pointy_'+opt.direction
		}).inject(this.pointyWrapper);
		this.positionPointer();
	},
	expose: function(){
		if ($(this).getStyle('display') != 'none' && $(document.body).hasChild($(this))) return $empty;
		$(this).setStyles({
			visibility: 'hidden',
			position: 'absolute'
		});
		var dispose;
		if (!document.body.hasChild($(this))) {
			$(this).inject(document.body);
			dispose = true;
		}
		return (function(){
			if (dispose) $(this).dispose();
			$(this).setStyles({
				visibility: 'visible',
				position: 'relative'
			});
		}).bind(this);
	},
	positionPointer: function(options){
		if (!this.pointer) return;

		var opt = this.options;
		var pos;
		var d = opt.direction;
		switch (d){
			case 12: case 1: case 11:
				pos = {
					edge: {x: 'center', y: 'bottom'},
					position: {
						x: d==12?'center':d==1?'right':'left', 
						y: 'top'
					},
					offset: {
						x: (d==12?0:d==1?-1:1)*opt.divotSize,
						y: 1
					}
				};
				break;
			case 2: case 3: case 4:
				pos = {
					edge: {x: 'left', y: 'center'},
					position: {
						x: 'right', 
						y: d==3?'center':d==2?'top':'bottom'
					},
					offset: {
						x: -1,
						y: (d==3?0:d==4?-1:1)*opt.divotSize
					}
				};
				break;
			case 5: case 6: case 7:
				pos = {
					edge: {x: 'center', y: 'top'},
					position: {
						x: d==6?'center':d==5?'right':'left', 
						y: 'bottom'
					},
					offset: {
						x: (d==6?0:d==5?-1:1)*opt.divotSize,
						y: -1
					}
				};
				break;
			case 8: case 9: case 10:
				pos = {
					edge: {x: 'right', y: 'center'},
					position: {
						x: 'left', 
						y: d==9?'center':d==10?'top':'bottom'
					},
					offset: {
						x: 1,
						y: (d==9?0:d==8?-1:1)*opt.divotSize
					}
				};
				break;
		};
		var putItBack = this.expose();
		this.pointer.setPosition($extend({
			relativeTo: this.pointyWrapper
		}, pos, options));
		putItBack();
	},
	setContent: function(){
		this.parent.apply(this, arguments);
		this.top[this.h1?'removeClass':'addClass']('noCaption');
		if (Browser.Engine.trident4) $(this).getElements('.bottom_ll, .bottom_lr').setStyle('font-size', 1); //IE6 bullshit
		this.positionPointer();
		return this;
	}
});

StickyWin.ui.pointy = function(caption, body, options){
	return $(new StickyWin.UI.Pointy(caption, body, options));
};

StickyWin.PointyTip = new Class({
	Extends: StickyWin,
	options: {
		point: "left",
		pointyOptions: {}
	},
	initialize: function(){
		var args = this.getArgs(arguments);
		this.setOptions(args.options);
		var popts = this.options.pointyOptions;
		var d = popts.direction;
		if (!d) {
			var map = {
				left: 9,
				right: 3,
				up: 12,
				down: 6
			};
			d = map[this.options.point];
			if (!d) d = this.options.point;
			popts.direction = d;
		}
		if (!popts.width) popts.width = this.options.width;
		this.pointy = new StickyWin.UI.Pointy(args.caption, args.body, popts);
		this.options.content = null;
		
		this.setOptions(args.options, this.getPositionSettings());
		this.parent(this.options);
		this.win.empty().adopt(this.pointy);
		this.attachHandlers(this.win);
		this.position();
	},
	getArgs: function(){
		return StickyWin.UI.getArgs.apply(this, arguments);
	},
	getPositionSettings: function(){
		var s = this.pointy.options.divotSize;
		var d = this.options.point;
		switch(d) {
			case "left": case 8: case 9: case 10:
				return {
					edge: {
						x: 'left', 
						y: d==10?'top':d==8?'bottom':'center'
					},
					position: {x: 'right', y: 'center'},
					offset: {
						x: s
					}
				};
			case "right": case 2:  case 3: case 4:
				return {
					edge: {
						x: 'right', 
						y: d==2?'top':d==4?'bottom':'center'
					},
					position: {x: 'left', y: 'center'},
					offset: {x: -s}
				};
			case "up": case 11: case 12: case 1:
				return {
					edge: { 
						x: d==11?'left':d==1?'right':'center', 
						y: 'top' 
					},
					position: {	x: 'center', y: 'bottom' },
					offset: {
						y: s,
						x: d==11?-s:d==1?s:0
					}
				};
			case "down": case 5: case 6: case 7:
				return {
					edge: {
						x: d==7?'left':d==5?'right':'center', 
						y: 'bottom'
					},
					position: {x: 'center', y: 'top'},
					offset: {y: -s}
				};
		};
	},
	setContent: function() {
		var args = this.getArgs(arguments);
		this.pointy.setContent(args.caption, args.body);
		[this.pointy.h1, this.pointy.body].each(this.attachHandlers, this);
		if (this.visible) this.position();
		return this;
	},
	showWin: function(){
		this.parent.apply(this, arguments);
		this.pointy.positionPointer();
	},
	position: function(){
		this.parent.apply(this, arguments);
		this.pointy.positionPointer();
	},
	attachHandlers: function(content) {
		if (!content) return;
		content.getElements('.'+this.options.closeClassName).addEvent('click', function(){ this.hide(); }.bind(this));
		content.getElements('.'+this.options.pinClassName).addEvent('click', function(){ this.togglepin(); }.bind(this));
	}
});

Tips.Pointy = new Class({
	Extends: Tips,
	options: {
		onShow: $empty,
		onHide: $empty,
		pointyTipOptions: {
			point: 11,
			width: 150,
			pointyOptions: {
				closeButton: false
			}
		}
	},
	initialize: function(){
		var params = Array.link(arguments, {options: Object.type, elements: $defined});
		this.setOptions(params.options);
		this.tip = new StickyWin.PointyTip($extend(this.options.pointyTipOptions, {
			showNow: false
		}));
		if (this.options.className) $(this.tip).addClass(this.options.className);
		if (params.elements) this.attach(params.elements);
	},
	elementEnter: function(event, element){

		var title = element.retrieve('tip:title');
		var text = element.retrieve('tip:text');
		
		this.tip.setContent(title, text);

		this.timer = $clear(this.timer);
		this.timer = this.show.delay(this.options.showDelay, this);

		this.position(element);
	},

	elementLeave: function(event){
		$clear(this.timer);
		this.timer = this.hide.delay(this.options.hideDelay, this);
	},

	elementMove: function(event){
		return; //always fixed
	},

	position: function(element){
		this.tip.setOptions({
			relativeTo: element
		});
		this.tip.position();
	},

	show: function(){
		this.fireEvent('show', this.tip);
		this.tip.show();
	},

	hide: function(){
		this.fireEvent('hide', this.tip);
		this.tip.hide();
	}

});

/*
Script: Waiter.js

Adds a semi-transparent overlay over a dom element with a spinnin ajax icon.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var Waiter = new Class({
	Implements: [Options, Events, Chain],
	options: {
		baseHref: 'http://www.cnet.com/html/rb/assets/global/waiter/',
		containerProps: {
			styles: {
				position: 'absolute',
				'text-align': 'center'
			},
			'class':'waiterContainer'
		},
		containerPosition: {},
		msg: false,
		msgProps: {
			styles: {
				'text-align': 'center',
				fontWeight: 'bold'
			},
			'class':'waiterMsg'
		},
		img: {
			src: 'waiter.gif',
			styles: {
				width: 24,
				height: 24
			},
			'class':'waiterImg'
		},
		layer:{
			styles: {
				width: 0,
				height: 0,
				position: 'absolute',
				zIndex: 999,
				display: 'none',
				opacity: 0.9,
				background: '#fff'
			},
			'class': 'waitingDiv'
		},
		useIframeShim: true,
		fxOptions: {},
		injectWhere: null
//	iframeShimOptions: {},
//	onShow: $empty
//	onHide: $empty
	},
	initialize: function(target, options){
		this.target = $(target)||$(document.body);
		this.setOptions(options);
		this.waiterContainer = new Element('div', this.options.containerProps);
		if (this.options.msg) {
			this.msgContainer = new Element('div', this.options.msgProps);
			this.waiterContainer.adopt(this.msgContainer);
			if (!$(this.options.msg)) this.msg = new Element('p').appendText(this.options.msg);
			else this.msg = $(this.options.msg);
			this.msgContainer.adopt(this.msg);
		}
		if (this.options.img) this.waiterImg = $(this.options.img.id) || new Element('img').inject(this.waiterContainer);
		this.waiterOverlay = $(this.options.layer.id) || new Element('div').adopt(this.waiterContainer);
		this.waiterOverlay.set(this.options.layer);
		this.place(target);
		try {
			if (this.options.useIframeShim) this.shim = new IframeShim(this.waiterOverlay, this.options.iframeShimOptions);
		} catch(e) {
			dbug.log("Waiter attempting to use IframeShim but failed; did you include IframeShim? Error: ", e);
			this.options.useIframeShim = false;
		}
		this.waiterFx = this.waiterFx || new Fx.Elements($$(this.waiterContainer, this.waiterOverlay), this.options.fxOptions);
	},
	place: function(target, where){
		var where = where || this.options.injectWhere || target == document.body ? 'inside' : 'after';
		this.waiterOverlay.inject(target, where);
	},
	toggle: function(element, show) {
		//the element or the default
		element = $(element) || $(this.active) || $(this.target);
		this.place(element);
		if (!$(element)) return this;
		if (this.active && element != this.active) return this.stop(this.start.bind(this, element));
		//if it's not active or show is explicit
		//or show is not explicitly set to false
		//start the effect
		if((!this.active || show) && show !== false) this.start(element);
		//else if it's active and show isn't explicitly set to true
		//stop the effect
		else if(this.active && !show) this.stop();
		return this;
	},
	reset: function(){
		this.waiterFx.cancel().set({
			0: { opacity:[0]},
			1: { opacity:[0]}
		});
	},
	start: function(element){
		this.reset();
		element = $(element) || $(this.target);
		this.place(element);
		if (this.options.img) {
			this.waiterImg.set($merge(this.options.img, {
				src: this.options.baseHref + this.options.img.src
			}));
		}
		
		var start = function() {
			var dim = element.getComputedSize();
			this.active = element;
			this.waiterOverlay.setStyles({
				width: this.options.layer.width||dim.totalWidth,
				height: this.options.layer.height||dim.totalHeight,
				display: 'block'
			}).setPosition({
				relativeTo: element,
				position: 'upperLeft'
			});
			this.waiterContainer.setPosition($merge({
				relativeTo: this.waiterOverlay
			}, this.options.containerPosition));
			if (this.options.useIframeShim) this.shim.show();
			this.waiterFx.start({
				0: { opacity:[1] },
				1: { opacity:[this.options.layer.styles.opacity]}
			}).chain(function(){
				if (this.active == element) this.fireEvent('onShow', element);
				this.callChain();
			}.bind(this));
		}.bind(this);

		if (this.active && this.active != element) this.stop(start);
		else start();
		
		return this;
	},
	stop: function(callback){
		if (!this.active) {
			if ($type(callback) == "function") callback.attempt();
			return this;
		}
		this.waiterFx.cancel();
		this.waiterFx.clearChain();
		//fade the waiter out
		this.waiterFx.start({
			0: { opacity:[0]},
			1: { opacity:[0]}
		}).chain(function(){
			this.active = null;
			this.waiterOverlay.hide();
			if (this.options.useIframeShim) this.shim.hide();
			this.fireEvent('onHide', this.active);
			this.callChain();
			this.clearChain();
			if ($type(callback) == "function") callback.attempt();
		}.bind(this));
		return this;
	}
});

if (typeof Request != "undefined" && Request.HTML) {
	Request.HTML = Class.refactor(Request.HTML, {
		options: {
			useWaiter: false,
			waiterOptions: {},
			waiterTarget: false
		},
		initialize: function(options){
			this._send = this.send;
			this.send = function(options){
				if(this.waiter) this.waiter.start().chain(this._send.bind(this, options));
				else this._send(options);
				return this;
			};
			this.parent(options);
			if (this.options.useWaiter && ($(this.options.update) || $(this.options.waiterTarget))) {
				this.waiter = new Waiter(this.options.waiterTarget || this.options.update, this.options.waiterOptions);
				['onComplete', 'onException', 'onCancel'].each(function(event){
					this.addEvent(event, this.waiter.stop.bind(this.waiter));
				}, this);
			}
		}
	});
}

Element.Properties.waiter = {

	set: function(options){
		var waiter = this.retrieve('waiter');
		return this.eliminate('wait').store('waiter:options');
	},

	get: function(options){
		if (options || !this.retrieve('waiter')){
			if (options || !this.retrieve('waiter:options')) this.set('waiter', options);
			this.store('waiter', new Waiter(this, this.retrieve('waiter:options')));
		}
		return this.retrieve('waiter');
	}

};

Element.implement({

	wait: function(options){
		this.get('waiter', options).start();
		return this;
	},
	
	release: function(){
		var opt = Array.link(arguments, {options: Object.type, callback: Function.type});
		this.get('waiter', opt.options).stop(opt.callback);
		return this;
	}

});

/*
Script: Collapsable.js
	Enables a dom element to, when clicked, hide or show (it toggles) another dom element. Kind of an Accordion for one item.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var Collapsable = new Class({
	Extends: Fx.Reveal,
	initialize: function(clicker, section, options) {
		this.clicker = $(clicker);
		this.section = $(section);
		this.parent(this.section, options);
		this.addEvents();
	},
	addEvents: function(){
		this.clicker.addEvent('click', this.toggle.bind(this));
	}
});

/*
Script: MooScroller.js

Recreates the standard scrollbar behavior for elements with overflow but using DOM elements so that the scroll bar elements are completely styleable by css.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var MooScroller = new Class({
	Implements: [Options, Events],
	options: {
		maxThumbSize: 10,
		mode: 'vertical',
		width: 0, //required only for mode: horizontal
		scrollSteps: 10,
		wheel: true,
		scrollLinks: {
			forward: 'scrollForward',
			back: 'scrollBack'
		},
		hideWhenNoOverflow: true
//		onScroll: $empty,
//		onPage: $empty
	},

	initialize: function(content, knob, options){
		this.setOptions(options);
		this.horz = (this.options.mode == "horizontal");

		this.content = $(content).setStyle('overflow', 'hidden');
		this.knob = $(knob);
		this.track = this.knob.getParent();
		this.setPositions();
		
		if(this.horz && this.options.width) {
			this.wrapper = new Element('div');
			this.content.getChildren().each(function(child){
				this.wrapper.adopt(child);
			}, this);
			this.wrapper.inject(this.content).setStyle('width', this.options.width);
		}
		

		this.bound = {
			'start': this.start.bind(this),
			'end': this.end.bind(this),
			'drag': this.drag.bind(this),
			'wheel': this.wheel.bind(this),
			'page': this.page.bind(this)
		};

		this.position = {};
		this.mouse = {};
		this.update();
		this.attach();
		
		var clearScroll = function (){
			$clear(this.scrolling);
		}.bind(this);
		['forward','back'].each(function(direction) {
			var lnk = $(this.options.scrollLinks[direction]);
			if(lnk) {
				lnk.addEvents({
					mousedown: function() {
						this.scrolling = this[direction].periodical(50, this);
					}.bind(this),
					mouseup: clearScroll.bind(this),
					click: clearScroll.bind(this)
				});
			}
		}, this);
		this.knob.addEvent('click', clearScroll.bind(this));
		window.addEvent('domready', function(){
			try {
				$(document.body).addEvent('mouseup', clearScroll.bind(this));
			}catch(e){}
		}.bind(this));
	},
	setPositions: function(){
		[this.track, this.knob].each(function(el){
			if (el.getStyle('position') == 'static') el.setStyle('position','relative');
		});

	},
	toElement: function(){
		return this.content;
	},
	update: function(){
		var plain = this.horz?'Width':'Height';
		this.contentSize = this.content['offset'+plain];
		this.contentScrollSize = this.content['scroll'+plain];
		this.trackSize = this.track['offset'+plain];

		this.contentRatio = this.contentSize / this.contentScrollSize;

		this.knobSize = (this.trackSize * this.contentRatio).limit(this.options.maxThumbSize, this.trackSize);

		if (this.options.hideWhenNoOverflow) {
			this.hidden = this.knobSize == this.trackSize;
			this.track.setStyle('opacity', this.hidden?0:1);
		}
		
		this.scrollRatio = this.contentScrollSize / this.trackSize;
		this.knob.setStyle(plain.toLowerCase(), this.knobSize);

		this.updateThumbFromContentScroll();
		this.updateContentFromThumbPosition();
	},

	updateContentFromThumbPosition: function(){
		this.content[this.horz?'scrollLeft':'scrollTop'] = this.position.now * this.scrollRatio;
	},

	updateThumbFromContentScroll: function(){
		this.position.now = (this.content[this.horz?'scrollLeft':'scrollTop'] / this.scrollRatio).limit(0, (this.trackSize - this.knobSize));
		this.knob.setStyle(this.horz?'left':'top', this.position.now);
	},

	attach: function(){
		this.knob.addEvent('mousedown', this.bound.start);
		if (this.options.scrollSteps) this.content.addEvent('mousewheel', this.bound.wheel);
		this.track.addEvent('mouseup', this.bound.page);
	},

	wheel: function(event){
		if (this.hidden) return;
		this.scroll(-(event.wheel * this.options.scrollSteps));
		this.updateThumbFromContentScroll();
		event.stop();
	},

	scroll: function(steps){
		steps = steps||this.options.scrollSteps;
		this.content[this.horz?'scrollLeft':'scrollTop'] += steps;
		this.updateThumbFromContentScroll();
		this.fireEvent('onScroll', steps);
	},
	forward: function(steps){
		this.scroll(steps);
	},
	back: function(steps){
		steps = steps||this.options.scrollSteps;
		this.scroll(-steps);
	},

	page: function(event){
		var axis = this.horz?'x':'y';
		var forward = (event.page[axis] > this.knob.getPosition()[axis]);
		this.scroll((forward?1:-1)*this.content['offset'+(this.horz?'Width':'Height')]);
		this.updateThumbFromContentScroll();
		this.fireEvent('onPage', forward);
		event.stop();
	},

	
	start: function(event){
		var axis = this.horz?'x':'y';
		this.mouse.start = event.page[axis];
		this.position.start = this.knob.getStyle(this.horz?'left':'top').toInt();
		document.addEvent('mousemove', this.bound.drag);
		document.addEvent('mouseup', this.bound.end);
		this.knob.addEvent('mouseup', this.bound.end);
		event.stop();
	},

	end: function(event){
		document.removeEvent('mousemove', this.bound.drag);
		document.removeEvent('mouseup', this.bound.end);
		this.knob.removeEvent('mouseup', this.bound.end);
		event.stop();
	},

	drag: function(event){
		var axis = this.horz?'x':'y';
		this.mouse.now = event.page[axis];
		this.position.now = (this.position.start + (this.mouse.now - this.mouse.start)).limit(0, (this.trackSize - this.knobSize));
		this.updateContentFromThumbPosition();
		this.updateThumbFromContentScroll();
		event.stop();
	}

});


/*
Script: TabSwapper.js

Handles the scripting for a common UI layout; the tabbed box.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var TabSwapper = new Class({
	Implements: [Options, Events],
	options: {
		selectedClass: 'tabSelected',
		mouseoverClass: 'tabOver',
		deselectedClass: '',
		rearrangeDOM: true,
		initPanel: 0, 
		smooth: false, 
		smoothSize: false,
		maxSize: null,
		effectOptions: {
			duration: 500
		},
		cookieName: null, 
		cookieDays: 999
//	onActive: $empty,
//	onActiveAfterFx: $empty,
//	onBackground: $empty
	},
	tabs: [],
	sections: [],
	clickers: [],
	sectionFx: [],
	initialize: function(options){
		this.setOptions(options);
		var prev = this.setup();
		if (prev) return prev;
		if(this.options.cookieName && this.recall()) this.show(this.recall().toInt());
		else this.show(this.options.initPanel);
	},
	setup: function(){
		var opt = this.options;
		sections = $$(opt.sections);
		tabs = $$(opt.tabs);
		if (tabs[0] && tabs[0].retrieve('tabSwapper')) return tabs[0].retrieve('tabSwapper');
		clickers = $$(opt.clickers);
		tabs.each(function(tab, index){
			this.addTab(tab, sections[index], clickers[index], index);
		}, this);
	},
	addTab: function(tab, section, clicker, index){
		tab = $(tab); clicker = $(clicker); section = $(section);
		//if the tab is already in the interface, just move it
		if(this.tabs.indexOf(tab) >= 0 && tab.retrieve('tabbered') 
			 && this.tabs.indexOf(tab) != index && this.options.rearrangeDOM) {
			this.moveTab(this.tabs.indexOf(tab), index);
			return this;
		}
		//if the index isn't specified, put the tab at the end
		if(!$defined(index)) index = this.tabs.length;
		//if this isn't the first item, and there's a tab
		//already in the interface at the index 1 less than this
		//insert this after that one
		if(index > 0 && this.tabs[index-1] && this.options.rearrangeDOM) {
			tab.inject(this.tabs[index-1], 'after');
			section.inject(this.tabs[index-1].retrieve('section'), 'after');
		}
		this.tabs.splice(index, 0, tab);
		clicker = clicker || tab;

		tab.addEvents({
			mouseout: function(){
				tab.removeClass(this.options.mouseoverClass);
			}.bind(this),
			mouseover: function(){
				tab.addClass(this.options.mouseoverClass);
			}.bind(this)
		});

		clicker.addEvent('click', function(e){
			e.preventDefault();
			this.show(index);
		}.bind(this));

		tab.store('tabbered', true);
		tab.store('section', section);
		tab.store('clicker', clicker);
		this.hideSection(index);
		return this;
	},
	removeTab: function(index){
		var now = this.tabs[this.now];
		if(this.now == index){
			if(index > 0) this.show(index - 1);
			else if (index < this.tabs.length) this.show(index + 1);
		}
		this.now = this.tabs.indexOf(now);
		return this;
	},
	moveTab: function(from, to){
		var tab = this.tabs[from];
		var clicker = tab.retrieve('clicker');
		var section = tab.retrieve('section');
		
		var toTab = this.tabs[to];
		var toClicker = toTab.retrieve('clicker');
		var toSection = toTab.retrieve('section');
		
		this.tabs.erase(tab).splice(to, 0, tab);

		tab.inject(toTab, 'before');
		clicker.inject(toClicker, 'before');
		section.inject(toSection, 'before');
		return this;
	},
	show: function(i){
		if (!$chk(this.now)) {
			this.tabs.each(function(tab, idx){
				if (i != idx) 
					this.hideSection(idx)
			}, this);
		}
		this.showSection(i).save(i);
		return this;
	},
	save: function(index){
		if(this.options.cookieName) 
			Cookie.write(this.options.cookieName, index, {duration:this.options.cookieDays});
		return this;
	},
	recall: function(){
		return (this.options.cookieName)?$pick(Cookie.read(this.options.cookieName), false): false;
	},
	hideSection: function(idx) {
		var tab = this.tabs[idx];
		if (!tab) return this;
		var sect = tab.retrieve('section');
		if (!sect) return this;
		if (sect.getStyle('display') != 'none') {
			this.lastHeight = sect.getSize().y;
			sect.setStyle('display', 'none');
			tab.swapClass(this.options.selectedClass, this.options.deselectedClass);
			this.fireEvent('onBackground', [idx, sect, tab]);
		}
		return this;
	},
	showSection: function(idx) {
		var tab = this.tabs[idx];
		if (!tab) return this;
		var sect = tab.retrieve('section');
		if (!sect) return this;
		var smoothOk = this.options.smooth && (!Browser.Engine.trident4 
										|| (Browser.Engine.trident4 && sect.fxOpacityOk()));
		if(this.now != idx) {
			if (!tab.retrieve('tabFx')) 
				tab.store('tabFx', new Fx.Morph(sect, this.options.effectOptions));
			var start = {
				display:'block',
				overflow: 'hidden'
			};
			if (smoothOk) start.opacity = 0;
			var effect = false;
			if(smoothOk) {
				effect = {opacity: 1};
			} else if (sect.getStyle('opacity').toInt() < 1) {
				sect.setStyle('opacity', 1);
				if (!this.options.smoothSize) 
					this.fireEvent('onActiveAfterFx', [idx, sect, tab]);
			}
			if (this.options.smoothSize) {
				var size = sect.getDimensions().height;
				if ($chk(this.options.maxSize) && this.options.maxSize < size) 
					size = this.options.maxSize;
				if (!effect) effect = {};
				effect.height = size;
			}
			if ($chk(this.now)) this.hideSection(this.now);
			if (this.options.smoothSize && this.lastHeight) start.height = this.lastHeight;
			sect.setStyles(start);
			if (effect) {
				tab.retrieve('tabFx').start(effect).chain(function(){
					this.fireEvent('onActiveAfterFx', [idx, sect, tab]);
					sect.setStyle("height", "auto");
				}.bind(this));
			}
			this.now = idx;
			this.fireEvent('onActive', [idx, sect, tab]);
		}
		tab.swapClass(this.options.deselectedClass, this.options.selectedClass);
		return this;
	}
});


/*
Script: Confirmer.js
	Fades a message in and out for the user to tell them that some event (like an ajax save) has occurred.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var Confirmer = new Class({
	Implements: [Options, Events],
	options: {
		reposition: true, //for elements already in the DOM
		//if position = false, just fade
		positionOptions: {
			relativeTo: false,
			position: 'upperRight', //see <Element.setPosition>
			offset: {x:-225,y:0},
			zIndex: 9999
		},
		msg: 'your changes have been saved', //string or dom element
		msgContainerSelector: '.body',
		delay: 250,
		pause: 1000,
		effectOptions:{
			duration: 500
		},
		prompterStyle:{
			padding: '2px 6px',
			border: '1px solid #9f0000',
			backgroundColor: '#f9d0d0',
			fontWeight: 'bold',
			color: '#000',
			width: 210
		}
//	onComplete: $empty
	},
	initialize: function(options){
			this.setOptions(options);
			this.options.positionOptions.relativeTo = $(this.options.positionOptions.relativeTo) || document.body;
			this.prompter = ($(this.options.msg))?$(this.options.msg):this.makePrompter(this.options.msg);
			if(this.options.reposition){
				this.prompter.setStyles({
					position: 'absolute',
					display: 'none',
					zIndex: this.options.positionOptions.zIndex
				});
				if(this.prompter.fxOpacityOk()) this.prompter.setStyle('opacity',0);
			} else if(this.prompter.fxOpacityOk()) this.prompter.setStyle('opacity',0);
			else this.prompter.setStyle('visibility','hidden');
			if (!this.prompter.getParent()){
				window.addEvent('domready', function(){
					this.prompter.inject(document.body);
				}.bind(this));
			}
		try {
			this.msgHolder = this.prompter.getElement(this.options.msgContainerSelector);
			if(!this.msgHolder) this.msgHolder = this.prompter;
		} catch(e){dbug.log(e)}
	},
	makePrompter: function(msg){
		return new Element('div').setStyles(this.options.prompterStyle).appendText(msg);
	},
	prompt: function(options){
		if(!this.paused)this.stop();
		var msg = (options)?options.msg:false;
		options = $merge(this.options, {saveAsDefault: false}, options||{});
		if ($(options.msg) && msg) this.msgHolder.empty().adopt(options.msg);
		else if (!$(options.msg) && options.msg) this.msgHolder.empty().appendText(options.msg);
		if(!this.paused) {
			if(options.reposition) this.position(options.positionOptions);
			(function(){
				this.timer = this.fade(options.pause);
			}).delay(options.delay, this);
		}
		if(options.saveAsDefault) this.setOptions(options);
		return this;
	},
	fade: function(pause){
		this.paused = true;
		pause = $pick(pause, this.options.pause);
		if(!this.fx && this.prompter.fxOpacityOk())
			this.fx = new Fx.Tween(this.prompter, $merge({property: 'opacity'}, this.options.effectOptions));
		if(this.options.reposition) this.prompter.setStyle('display','block');
		if(this.prompter.fxOpacityOk()){
			this.prompter.setStyle('visibility','visible');
			this.fx.start(0,1).chain(function(){
				this.timer = (function(){
					this.fx.start(0).chain(function(){
						if(this.options.reposition) this.prompter.hide();
						this.paused = false;
					}.bind(this));
				}).delay(pause, this);
			}.bind(this));
		} else {
			this.prompter.setStyle('visibility','visible');
			this.timer = (function(){
				this.prompter.setStyle('visibility','hidden');
				this.fireEvent('onComplete');
				this.paused = false;
			}).delay(pause+this.options.effectOptions.duration, this);
		}
		return this;
	},
	stop: function(){	
		this.paused = false;
		$clear(this.timer);
		if(this.fx) this.fx.set(0);
		if(this.options.reposition) this.prompter.hide();
		return this;
	},
	position: function(positionOptions){
		this.prompter.setPosition($merge(this.options.positionOptions, positionOptions));
		return this;
	}
});


/*
Script: FormValidator.js
	A css-class based form validation system.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var InputValidator = new Class({
	Implements: [Options],
	options: {
		errorMsg: 'Validation failed.',
		test: function(field){return true;}
	},
	initialize: function(className, options){
		this.setOptions(options);
		this.className = className;
	},
	test: function(field, props){
		if($(field)) return this.options.test($(field), props||this.getProps(field));
		else return false;
	},
	getError: function(field, props){
		var err = this.options.errorMsg;
		if($type(err) == "function") err = err($(field), props||this.getProps(field));
		return err;
	},
	getProps: function(field){
		if (!$(field)) return {};
		return field.get('validatorProps');
	}
});

Element.Properties.validatorProps = {

	set: function(props){
		return this.eliminate('validatorProps').store('validatorProps', props);
	},

	get: function(props){
		if (props) this.set(props);
		if (this.retrieve('validatorProps')) return this.retrieve('validatorProps');
		if(this.getProperty('validatorProps')){
			try {
				this.store('validatorProps', JSON.decode(this.getProperty('validatorProps')));
			}catch(e){ return {}}
		} else {
			var vals = this.get('class').split(' ').filter(function(cls) {
				//return cls.match(/[a-z].*\[\'.*\'\]/ig);
				return cls.test(':');
			});
			if (!vals.length) {
				this.store('validatorProps', {});
			} else {
				props = {};
				vals.each(function(cls){
					var split = cls.indexOf(':');
					props[cls.substring(0, split)] = JSON.decode(cls.substring(split+1, cls.length))
				});
				this.store('validatorProps', props);
			}
		}
		return this.retrieve('validatorProps');
	}

};

var FormValidator = new Class({
	Implements:[Options, Events],
	options: {
		fieldSelectors:"input, select, textarea",
		ignoreHidden: true,
		useTitles:false,
		evaluateOnSubmit:true,
		evaluateFieldsOnBlur: true,
		evaluateFieldsOnChange: true,
		serial: true,
		stopOnFailure: true,
		scrollToErrorsOnSubmit: true,
		warningPrefix: function(){
			return FormValidator.resources[FormValidator.language].warningPrefix || 'Warning: ';
		},
		errorPrefix: function(){
			return FormValidator.resources[FormValidator.language].errorPrefix || 'Error: ';
		}
//	onFormValidate: function(isValid, form){},
//	onElementValidate: function(isValid, field){}
	},
	initialize: function(form, options){
		this.setOptions(options);
		this.form = $(form);
		this.form.store('validator', this);
		this.warningPrefix = $lambda(this.options.warningPrefix)();
		this.errorPrefix = $lambda(this.options.errorPrefix)();

		if(this.options.evaluateOnSubmit) this.form.addEvent('submit', this.onSubmit.bind(this));
		if(this.options.evaluateFieldsOnBlur) this.watchFields();
	},
	toElement: function(){
		return this.form;
	},
	getFields: function(){
		return this.fields = this.form.getElements(this.options.fieldSelectors);
	},
	watchFields: function(){
		this.getFields().each(function(el){
				el.addEvent('blur', this.validateField.pass([el, false], this));
			if(this.options.evaluateFieldsOnChange)
				el.addEvent('change', this.validateField.pass([el, true], this));
		}, this);
	},
	onSubmit: function(event){
		if(!this.validate(event) && event) event.preventDefault();
		else this.reset();
	},
	reset: function() {
		this.getFields().each(this.resetField, this);
		return this;
	}, 
	validate: function(event) {
		var result = this.getFields().map(function(field) { 
			return this.validateField(field, true);
		}, this).every(function(v){ return v;});
		this.fireEvent('onFormValidate', [result, this.form, event]);
		if (this.options.stopOnFailure && !result && event) event.preventDefault();
		if (this.options.scrollToErrorsOnSubmit && !result) {
			var par = this.form.getParent();
			var isScrolled = function(p){
				return p.getScrollSize().y != p.getSize().y
			};
			var scrolls;
			while (par != document.body && !isScrolled(par)) {
				par = par.getParent();
			};
			var fx = par.retrieve('fvScroller');
			if (!fx && window.Fx) {
				fx = new Fx.Scroll(par, {
					transition: 'quad:out',
					offset: {
						y: -20
					}
				});
				par.store('fvScroller', fx);
			}
			var failed = this.form.getElement('.validation-failed');
			if (failed) {
				if (fx) fx.toElement(failed);
				else par.scrollTo(par.getScroll().x, failed.getPosition(par).y - 20);
			}
		}
		return result;
	},
	validateField: function(field, force){
		if (this.paused) return true;
		field = $(field);
		var passed = !field.hasClass('validation-failed');
		var failed, warned;
		if (this.options.serial && !force) {
			failed = this.form.getElement('.validation-failed');
			warned = this.form.getElement('.warning');
		}
		if(field && (!failed || force || field.hasClass('validation-failed') || (failed && !this.options.serial))){
			var validators = field.className.split(" ").some(function(cn){
				return this.getValidator(cn);
			}, this);
			var validatorsFailed = [];
			field.className.split(" ").each(function(className){
				if (!this.test(className,field)) validatorsFailed.include(className);
			}, this);
			passed = validatorsFailed.length === 0;
			if (validators && !field.hasClass('warnOnly')){
				if(passed) {
					field.addClass('validation-passed').removeClass('validation-failed');
					this.fireEvent('onElementPass', field);
				} else {
					field.addClass('validation-failed').removeClass('validation-passed');
					this.fireEvent('onElementFail', [field, failed]);
				}
			}
			if(!warned) {
				var warnings = field.className.split(" ").some(function(cn){
					if(cn.test('^warn-') || field.hasClass('warnOnly')) 
						return this.getValidator(cn.replace(/^warn-/,""));
					else return null;
				}, this);
				field.removeClass('warning');
				var warnResult = field.className.split(" ").map(function(cn){
					if(cn.test('^warn-') || field.hasClass('warnOnly')) 
						return this.test(cn.replace(/^warn-/,""), field, true);
					else return null;
				}, this);
			}
		}
		return passed;
	},
	getPropName: function(className){
		return 'advice'+className;
	},
	test: function(className, field, warn){
		field = $(field);
		if(field.hasClass('ignoreValidation')) return true;
		warn = $pick(warn, false);
		if(field.hasClass('warnOnly')) warn = true;
		var isValid = true;
		var validator = this.getValidator(className);
		if(validator && this.isVisible(field)) {
			isValid = validator.test(field);
			if(!isValid && validator.getError(field)){
				if(warn) field.addClass('warning');
				var advice = this.makeAdvice(className, field, validator.getError(field), warn);
				this.insertAdvice(advice, field);
				this.showAdvice(className, field);
			} else this.hideAdvice(className, field);
			this.fireEvent('onElementValidate', [isValid, field, className]);
		}

		if(warn) return true;
		return isValid;
	},
	getAllAdviceMessages: function(field, force) {
		var advice = [];
		if (field.hasClass('ignoreValidation') && !force) return advice;
		var validators = field.className.split(" ").some(function(cn){
			var warner = cn.test('^warn-') || field.hasClass('warnOnly');
			if (warner) cn = cn.replace(/^warn-/,"");
			var validator = this.getValidator(cn);
			if (!validator) return;
			advice.push({
				message: validator.getError(field),
				warnOnly: warner,
				passed: validator.test(),
				validator: validator
			});
		}, this);
		return advice;
	},
	showAdvice: function(className, field){
		var advice = this.getAdvice(className, field);
		if(advice && !field.retrieve(this.getPropName(className))
			 && (advice.getStyle('display') == "none" 
			 || advice.getStyle('visiblity') == "hidden" 
			 || advice.getStyle('opacity')==0)){
			field.store(this.getPropName(className), true);
			if(advice.reveal) advice.reveal();
			else advice.setStyle('display','block');
		}
	},
	hideAdvice: function(className, field){
		var advice = this.getAdvice(className, field);
		if(advice && field.retrieve(this.getPropName(className))) {
			field.store(this.getPropName(className), false);
			//if Fx.Reveal.js is present, transition the advice out
			if(advice.dissolve) advice.dissolve();
			else advice.setStyle('display','none');
		}
	},
	isVisible : function(field) {
		if (!this.options.ignoreHidden) return true;
		while(field != document.body) {
			if($(field).getStyle('display') == "none") return false;
			field = field.getParent();
		}
		return true;
	},
	getAdvice: function(className, field) {
		return field.retrieve('advice-'+className);
	},
	makeAdvice: function(className, field, error, warn){
		var errorMsg = (warn)?this.warningPrefix:this.errorPrefix;
				errorMsg += (this.options.useTitles) ? field.title || error:error;
		var advice = this.getAdvice(className, field);
		if(!advice){
			var cssClass = (warn)?'warning-advice':'validation-advice';
			advice = new Element('div', {
				text: errorMsg,
				styles: { display: 'none' },
				id: 'advice-'+className+'-'+this.getFieldId(field)
			}).addClass(cssClass);
		} else{
			advice.set('html', errorMsg);
		}
		field.store('advice-'+className, advice);
		return advice;
	},
	insertAdvice: function(advice, field){
		//Check for error position prop
		var props = field.get('validatorProps');
		//Build advice
		if (!props.msgPos || !$(props.msgPos)) {
			switch (field.type.toLowerCase()) {
				case 'radio':
					var p = field.getParent().adopt(advice);
					break;
				default: 
					advice.inject($(field), 'after');
			};
		} else {
			$(props.msgPos).grab(advice);
		}
	},
	getFieldId : function(field) {
		return field.id ? field.id : field.id = "input_"+field.name;
	},
	resetField: function(field) {
		field = $(field);
		if(field) {
			var cn = field.className.split(" ");
			cn.each(function(className) {
				if(className.test('^warn-')) className = className.replace(/^warn-/,"");
				var prop = this.getPropName(className);
				if(field.retrieve(prop)) this.hideAdvice(className, field);
				field.removeClass('validation-failed');
				field.removeClass('warning');
				field.removeClass('validation-passed');
			}, this);
		}
		return this;
	},
	stop: function(){
		this.paused = true;
		return this;
	},
	start: function(){
		this.paused = false;
		return this;
	},
	ignoreField: function(field, warn){
		field = $(field);
		if(field){
			this.enforceField(field);
			if(warn) field.addClass('warnOnly');
			else field.addClass('ignoreValidation');
		}
		return this;
	},
	enforceField: function(field){
		field = $(field);
		if(field) field.removeClass('warnOnly').removeClass('ignoreValidation');
		return this;
	}
});

FormValidator.resources = {
	enUS: {
		required:'This field is required.',
		minLength:'Please enter at least {minLength} characters (you entered {length} characters).',
		maxLength:'Please enter no more than {maxLength} characters (you entered {length} characters).',
		integer:'Please enter an integer in this field. Numbers with decimals (e.g. 1.25) are not permitted.',
		numeric:'Please enter only numeric values in this field (i.e. "1" or "1.1" or "-1" or "-1.1").',
		digits:'Please use numbers and punctuation only in this field (for example, a phone number with dashes or dots is permitted).',
		alpha:'Please use letters only (a-z) with in this field. No spaces or other characters are allowed.',
		alphanum:'Please use only letters (a-z) or numbers (0-9) only in this field. No spaces or other characters are allowed.',
		dateSuchAs:'Please enter a valid date such as {date}',
		dateInFormatMDY:'Please enter a valid date such as MM/DD/YYYY (i.e. "12/31/1999")',
		email:'Please enter a valid email address. For example "fred@domain.com".',
		url:'Please enter a valid URL such as http://www.google.com.',
		currencyDollar:'Please enter a valid $ amount. For example $100.00 .',
		oneRequired:'Please enter something for at least one of these inputs.',
		errorPrefix: 'Error: ',
		warningPrefix: 'Warning: '
	}
};
FormValidator.language = "enUS";
FormValidator.getMsg = function(key, language){
	return FormValidator.resources[language||FormValidator.language][key];
};

FormValidator.adders = {
	validators:{},
	add : function(className, options) {
		this.validators[className] = new InputValidator(className, options);
		//if this is a class
		//extend these validators into it
		if(!this.initialize){
			this.implement({
				validators: this.validators
			});
		}
	},
	addAllThese : function(validators) {
		$A(validators).each(function(validator) {
			this.add(validator[0], validator[1]);
		}, this);
	},
	getValidator: function(className){
		return this.validators[className.split(":")[0]];
	}
};
$extend(FormValidator, FormValidator.adders);
FormValidator.implement(FormValidator.adders);

FormValidator.add('IsEmpty', {
	errorMsg: false,
	test: function(element) { 
		if(element.type == "select-one"||element.type == "select")
			return !(element.selectedIndex >= 0 && element.options[element.selectedIndex].value != "");
		else
			return ((element.get('value') == null) || (element.get('value').length == 0));
	}
});

FormValidator.addAllThese([
	['required', {
		errorMsg: function(){
			return FormValidator.getMsg('required');
		},
		test: function(element) { 
			return !FormValidator.getValidator('IsEmpty').test(element); 
		}
	}],
	['minLength', {
		errorMsg: function(element, props){
			if($type(props.minLength))
				return FormValidator.getMsg('minLength').substitute({minLength:props.minLength,length:element.get('value').length });
			else return '';
		}, 
		test: function(element, props) {
			if($type(props.minLength)) return (element.get('value').length >= $pick(props.minLength, 0));
			else return true;
		}
	}],
	['maxLength', {
		errorMsg: function(element, props){
			//props is {maxLength:10}
			if($type(props.maxLength))
				return FormValidator.getMsg('maxLength').substitute({maxLength:props.maxLength,length:element.get('value').length });
			else return '';
		}, 
		test: function(element, props) {
			//if the value is <= than the maxLength value, element passes test
			return (element.get('value').length <= $pick(props.maxLength, 10000));
		}
	}],
	['validate-integer', {
		errorMsg: FormValidator.getMsg.pass('integer'),
		test: function(element) {
			return FormValidator.getValidator('IsEmpty').test(element) || /^-?[1-9]\d*$/.test(element.get('value'));
		}
	}],
	['validate-numeric', {
		errorMsg: FormValidator.getMsg.pass('numeric'), 
		test: function(element) {
			return FormValidator.getValidator('IsEmpty').test(element) || 
				/^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/.test(element.get('value'));
		}
	}],
	['validate-digits', {
		errorMsg: FormValidator.getMsg.pass('digits'), 
		test: function(element) {
			return FormValidator.getValidator('IsEmpty').test(element) || (/^[\d() .:\-\+#]+$/.test(element.get('value')));
		}
	}],
	['validate-alpha', {
		errorMsg: FormValidator.getMsg.pass('alpha'), 
		test: function (element) {
			return FormValidator.getValidator('IsEmpty').test(element) ||  /^[a-zA-Z]+$/.test(element.get('value'))
		}
	}],
	['validate-alphanum', {
		errorMsg: FormValidator.getMsg.pass('alphanum'), 
		test: function(element) {
			return FormValidator.getValidator('IsEmpty').test(element) || !/\W/.test(element.get('value'))
		}
	}],
	['validate-date', {
		errorMsg: function(element, props) {
			if (Date.parse) {
				var format = props.dateFormat || "%x";
				return FormValidator.getMsg('dateSuchAs').substitute({date:new Date().format(format)});
			} else {
				return FormValidator.getMsg('dateInFormatMDY');
			}
		},
		test: function(element, props) {
			if(FormValidator.getValidator('IsEmpty').test(element)) return true;
			if (Date.parse) {
				var format = props.dateFormat || "%x";
				var d = Date.parse(element.get('value'));
				var formatted = d.format(format);
				if (formatted != "invalid date") element.set('value', formatted);
				return !isNaN(d);
			} else {
			var regex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
			if(!regex.test(element.get('value'))) return false;
			var d = new Date(element.get('value').replace(regex, '$1/$2/$3'));
			return (parseInt(RegExp.$1, 10) == (1+d.getMonth())) && 
			(parseInt(RegExp.$2, 10) == d.getDate()) && 
			(parseInt(RegExp.$3, 10) == d.getFullYear() );
			}
		}
	}],
	['validate-email', {
		errorMsg: FormValidator.getMsg.pass('email'), 
		test: function (element) {
			return FormValidator.getValidator('IsEmpty').test(element) || /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(element.get('value'));
		}
	}],
	['validate-url', {
		errorMsg: FormValidator.getMsg.pass('url'), 
		test: function (element) {
			return FormValidator.getValidator('IsEmpty').test(element) || /^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i.test(element.get('value'));
		}
	}],
	['validate-currency-dollar', {
		errorMsg: FormValidator.getMsg.pass('currencyDollar'), 
		test: function(element) {
			// [$]1[##][,###]+[.##]
			// [$]1###+[.##]
			// [$]0.##
			// [$].##
			return FormValidator.getValidator('IsEmpty').test(element) ||  /^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/.test(element.get('value'));
		}
	}],
	['validate-one-required', {
		errorMsg: FormValidator.getMsg.pass('oneRequired'), 
		test: function (element) {
			var p = element.parentNode;
			return p.getElements('input').some(function(el) {
				if (['checkbox', 'radio'].contains(el.get('type'))) return el.get('checked');
				return el.get('value');
			});
		}
	}]
]);

Element.Properties.validator = {

	set: function(options){
		var validator = this.retrieve('validator');
		if (validator) validator.setOptions(options);
		return this.store('validator:options');
	},

	get: function(options){
		if (options || !this.retrieve('validator')){
			if (options || !this.retrieve('validator:options')) this.set('validator', options);
			this.store('validator', new FormValidator(this, this.retrieve('validator:options')));
		}
		return this.retrieve('validator');
	}

};

Element.implement({
	validate: function(options){
		this.set('validator', options);
		return this.get('validator', options).validate();
	}
});

/*
Script: OverText.js
	Shows text over an input that disappears when the user clicks into it. The text remains hidden if the user adds a value.

License:
	http://www.clientcide.com/wiki/cnet-libraries#license
*/
var OverText = new Class({
	Implements: [Options, Events],
	options: {
//	textOverride: null,
		positionOptions: {
			position:"upperLeft",
			edge:"upperLeft",
			offset: {
				x: 4,
				y: 2
			}
		},
		poll: false,
		pollInterval: 250
//	onTextHide: $empty,
//	onTextShow: $empty
	},
	overTxtEls: [],
	initialize: function(inputs, options) {
		this.setOptions(options);
		$G(inputs).each(this.addElement, this);
		OverText.instances.push(this);
		if (this.options.poll) this.poll();
	},
	addElement: function(el){
		if (el.retrieve('OverText')) return;
		var val = this.options.textOverride || el.get('alt') || el.get('title');
		if (!val) return;
		this.overTxtEls.push(el);
		var txt = new Element('div', {
		  'class': 'overTxtDiv',
			styles: {
				lineHeight: 'normal',
				position: 'absolute'
			},
		  html: val,
		  events: {
		    click: this.hideTxt.pass([el, true], this)
		  }
		}).inject(el, 'after');
		el.addEvents({
			focus: this.hideTxt.pass([el, true], this),
			blur: this.testOverTxt.pass(el, this),
			change: this.testOverTxt.pass(el, this)
		}).store('OverTextDiv', txt).store('OverText', this);
		window.addEvent('resize', this.repositionAll.bind(this));
		this.testOverTxt(el);
		this.repositionOverTxt(el);
	},
	startPolling: function(){
		this.pollingPaused = false;
		return this.poll();
	},
	poll: function(stop) {
		//start immediately
		//pause on focus
		//resumeon blur
		if (this.poller && !stop) return this;
		var test = function(){
			if (this.pollingPaused == true) return;
			this.overTxtEls.each(function(el){
				if (el.retrieve('ot_paused')) return;
				this.testOverTxt(el);
			}, this);
		}.bind(this);
		if (stop) $clear(this.poller);
		else this.poller = test.periodical(this.options.pollInterval, this);
		return this;
	},
	stopPolling: function(){
		this.pollingPaused = true;
		return this.poll(true);
	},
	hideTxt: function(el, focus){
		var txt = el.retrieve('OverTextDiv');
		if (txt && txt.isVisible() && !el.get('disabled')) {
			txt.hide(); 
			try {
				if (focus) el.fireEvent('focus').focus();
			} catch(e){} //IE barfs if you call focus on hidden elements
			this.fireEvent('onTextHide', [txt, el]);
			el.store('ot_paused', true);
		}
		return this;
	},
	showTxt: function(el){
		var txt = el.retrieve('OverTextDiv');
		if (txt && !txt.isVisible()) {
			txt.show();
			this.fireEvent('onTextShow', [txt, el]);
			el.store('ot_paused', false);
		}
		return this;
	},
	testOverTxt: function(el){
		if (el.get('value')) this.hideTxt(el);
		else this.showTxt(el);	
	},
	repositionAll: function(){
		this.overTxtEls.each(this.repositionOverTxt.bind(this));
		return this;
	},
	repositionOverTxt: function (el){
		if (!el) return;
		try {
			var txt = el.retrieve('OverTextDiv');
			if (!txt || !el.getParent()) return;
			this.testOverTxt(el);
			txt.setPosition($merge(this.options.positionOptions, {relativeTo: el}));
			if (el.offsetHeight) this.testOverTxt(el);
			else this.hideTxt(el);
		} catch(e){
			dbug.log('overTxt error: ', e);
		}
		return this;
	}
});
OverText.instances = [];
OverText.update = function(){
	return OverText.instances.map(function(ot){
		return ot.repositionAll();
	});
};

/**
 * Autocompleter
 *
 * @version		1.1.1
 *
 * @todo: Caching, no-result handling!
 *
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */
var Autocompleter = {};

var OverlayFix = IframeShim;

Autocompleter.Base = new Class({
	
	Implements: [Options, Events],
	
	options: {
		minLength: 1,
		markQuery: true,
		width: 'inherit',
		maxChoices: 10,
//		injectChoice: null,
//		customChoices: null,
		className: 'autocompleter-choices',
		zIndex: 42,
		delay: 400,
		observerOptions: {},
		fxOptions: {},
//		onSelection: $empty,
//		onShow: $empty,
//		onHide: $empty,
//		onBlur: $empty,
//		onFocus: $empty,

		autoSubmit: false,
		overflow: false,
		overflowMargin: 25,
		selectFirst: false,
		filter: null,
		filterCase: false,
		filterSubset: false,
		forceSelect: false,
		selectMode: true,
		choicesMatch: null,

		multiple: false,
		separator: ', ',
		separatorSplit: /\s*[,;]\s*/,
		autoTrim: true,
		allowDupes: false,

		cache: true,
		relative: false
	},

	initialize: function(element, options) {
		this.element = $(element);
		this.setOptions(options);
		this.build();
		this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({
			'delay': this.options.delay
		}, this.options.observerOptions));
		this.queryValue = null;
		if (this.options.filter) this.filter = this.options.filter.bind(this);
		var mode = this.options.selectMode;
		this.typeAhead = (mode == 'type-ahead');
		this.selectMode = (mode === true) ? 'selection' : mode;
		this.cached = [];
	},

	/**
	 * build - Initialize DOM
	 *
	 * Builds the html structure for choices and appends the events to the element.
	 * Override this function to modify the html generation.
	 */
	build: function() {
		if ($(this.options.customChoices)) {
			this.choices = this.options.customChoices;
		} else {
			this.choices = new Element('ul', {
				'class': this.options.className,
				'styles': {
					'zIndex': this.options.zIndex
				}
			}).inject(document.body);
			this.relative = false;
			if (this.options.relative || this.element.getOffsetParent() != document.body) {
				this.choices.inject(this.element, 'after');
				this.relative = this.element.getOffsetParent();
			}
			this.fix = new OverlayFix(this.choices);
		}
		if (!this.options.separator.test(this.options.separatorSplit)) {
			this.options.separatorSplit = this.options.separator;
		}
		this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, $merge({
			'property': 'opacity',
			'link': 'cancel',
			'duration': 200
		}, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0);
		this.element.setProperty('autocomplete', 'off')
			.addEvent((Browser.Engine.trident || Browser.Engine.webkit) ? 'keydown' : 'keypress', this.onCommand.bind(this))
			.addEvent('click', this.onCommand.bind(this, [false]))
			.addEvent('focus', this.toggleFocus.create({bind: this, arguments: true, delay: 100}));
			//.addEvent('blur', this.toggleFocus.create({bind: this, arguments: false, delay: 100}));
		document.addEvent('click', function(e){
			if (e.target != this.choices) this.toggleFocus(false);
		}.bind(this));
	},

	destroy: function() {
		if (this.fix) this.fix.dispose();
		this.choices = this.selected = this.choices.destroy();
	},

	toggleFocus: function(state) {
		this.focussed = state;
		if (!state) this.hideChoices(true);
		this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]);
	},

	onCommand: function(e) {
		if (!e && this.focussed) return this.prefetch();
		if (e && e.key && !e.shift) {
			switch (e.key) {
				case 'enter':
					if (this.element.value != this.opted) return true;
					if (this.selected && this.visible) {
						this.choiceSelect(this.selected);
						return !!(this.options.autoSubmit);
					}
					break;
				case 'up': case 'down':
					if (!this.prefetch() && this.queryValue !== null) {
						var up = (e.key == 'up');
						this.choiceOver((this.selected || this.choices)[
							(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')
						](this.options.choicesMatch), true);
					}
					return false;
				case 'esc': case 'tab':
					this.hideChoices(true);
					break;
			}
		}
		return true;
	},

	setSelection: function(finish) {
		var input = this.selected.inputValue, value = input;
		var start = this.queryValue.length, end = input.length;
		if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0;
		if (this.options.multiple) {
			var split = this.options.separatorSplit;
			value = this.element.value;
			start += this.queryIndex;
			end += this.queryIndex;
			var old = value.substr(this.queryIndex).split(split, 1)[0];
			value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length);
			if (finish) {
				var space = /[^\s,]+/;
				var tokens = value.split(this.options.separatorSplit).filter(space.test, space);
				if (!this.options.allowDupes) tokens = [].combine(tokens);
				var sep = this.options.separator;
				value = tokens.join(sep) + sep;
				end = value.length;
			}
		}
		this.observer.setValue(value);
		this.opted = value;
		if (finish || this.selectMode == 'pick') start = end;
		this.element.selectRange(start, end);
		this.fireEvent('onSelection', [this.element, this.selected, value, input]);
	},

	showChoices: function() {
		var match = this.options.choicesMatch, first = this.choices.getFirst(match);
		this.selected = this.selectedValue = null;
		if (this.fix) {
			var pos = this.element.getCoordinates(this.relative), width = this.options.width || 'auto';
			this.choices.setStyles({
				'left': pos.left,
				'top': pos.bottom,
				'width': (width === true || width == 'inherit') ? pos.width : width
			});
		}
		if (!first) return;
		if (!this.visible) {
			this.visible = true;
			this.choices.setStyle('display', '');
			if (this.fx) this.fx.start(1);
			this.fireEvent('onShow', [this.element, this.choices]);
		}
		if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead);
		var items = this.choices.getChildren(match), max = this.options.maxChoices;
		var styles = {'overflowY': 'hidden', 'height': ''};
		this.overflown = false;
		if (items.length > max) {
			var item = items[max - 1];
			styles.overflowY = 'scroll';
			styles.height = item.getCoordinates(this.choices).bottom;
			this.overflown = true;
		};
		this.choices.setStyles(styles);
		this.fix.show();
	},

	hideChoices: function(clear) {
		if (clear) {
			var value = this.element.value;
			if (this.options.forceSelect) value = this.opted;
			if (this.options.autoTrim) {
				value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator);
			}
			this.observer.setValue(value);
		}
		if (!this.visible) return;
		this.visible = false;
		this.observer.clear();
		var hide = function(){
			this.choices.setStyle('display', 'none');
			this.fix.hide();
		}.bind(this);
		if (this.fx) this.fx.start(0).chain(hide);
		else hide();
		this.fireEvent('onHide', [this.element, this.choices]);
	},

	prefetch: function() {
		var value = this.element.value, query = value;
		if (this.options.multiple) {
			var split = this.options.separatorSplit;
			var values = value.split(split);
			var index = this.element.getCaretPosition();
			var toIndex = value.substr(0, index).split(split);
			var last = toIndex.length - 1;
			index -= toIndex[last].length;
			query = values[last];
		}
		if (query.length < this.options.minLength) {
			this.hideChoices();
		} else {
			if (query === this.queryValue || (this.visible && query == this.selectedValue)) {
				if (this.visible) return false;
				this.showChoices();
			} else {
				this.queryValue = query;
				this.queryIndex = index;
				if (!this.fetchCached()) this.query();
			}
		}
		return true;
	},

	fetchCached: function() {
		return false;
		if (!this.options.cache
			|| !this.cached
			|| !this.cached.length
			|| this.cached.length >= this.options.maxChoices
			|| this.queryValue) return false;
		this.update(this.filter(this.cached));
		return true;
	},

	update: function(tokens) {
		this.choices.empty();
		this.cached = tokens;
		if (!tokens || !tokens.length) {
			this.hideChoices();
		} else {
			if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices;
			tokens.each(this.options.injectChoice || function(token){
				var choice = new Element('li', {'html': this.markQueryValue(token)});
				choice.inputValue = token;
				this.addChoiceEvents(choice).inject(this.choices);
			}, this);
			this.showChoices();
		}
	},

	choiceOver: function(choice, selection) {
		if (!choice || choice == this.selected) return;
		if (this.selected) this.selected.removeClass('autocompleter-selected');
		this.selected = choice.addClass('autocompleter-selected');
		this.fireEvent('onSelect', [this.element, this.selected, selection]);
		if (!selection) return;
		this.selectedValue = this.selected.inputValue;
		if (this.overflown) {
			var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin,
				top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height;
			if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0);
			else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom);
		}
		if (this.selectMode) this.setSelection();
	},

	choiceSelect: function(choice) {
		if (choice) this.choiceOver(choice);
		this.setSelection(true);
		this.queryValue = false;
		this.hideChoices();
	},

	filter: function(tokens) {
		var regex = new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i');
		return (tokens || this.tokens).filter(regex.test, regex);
	},

	/**
	 * markQueryValue
	 *
	 * Marks the queried word in the given string with <span class="autocompleter-queried">*</span>
	 * Call this i.e. from your custom parseChoices, same for addChoiceEvents
	 *
	 * @param		{String} Text
	 * @return		{String} Text
	 */
	markQueryValue: function(str) {
		return (!this.options.markQuery || !this.queryValue) ? str
			: str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '<span class="autocompleter-queried">$1</span>');
	},

	/**
	 * addChoiceEvents
	 *
	 * Appends the needed event handlers for a choice-entry to the given element.
	 *
	 * @param		{Element} Choice entry
	 * @return		{Element} Choice entry
	 */
	addChoiceEvents: function(el) {
		return el.addEvents({
			'mouseover': this.choiceOver.bind(this, [el]),
			'click': this.choiceSelect.bind(this, [el])
		});
	}
});


/**
 * Autocompleter.Remote
 *
 * @version		1.1.1
 *
 * @todo: Caching, no-result handling!
 *
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */

Autocompleter.Ajax = {};

Autocompleter.Ajax.Base = new Class({

	Extends: Autocompleter.Base,

	options: {
		postVar: 'value',
		postData: {},
		ajaxOptions: {},
		onRequest: $empty,
		onComplete: $empty
	},

	initialize: function(element, options) {
		this.parent(element, options);
		var indicator = $(this.options.indicator);
		if (indicator) {
			this.addEvents({
				'onRequest': indicator.show.bind(indicator),
				'onComplete': indicator.hide.bind(indicator)
			}, true);
		}
	},

	query: function(){
		var data = $unlink(this.options.postData);
		data[this.options.postVar] = this.queryValue;
		this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]);
		this.request.send({'data': data});
	},

	/**
	 * queryResponse - abstract
	 *
	 * Inherated classes have to extend this function and use this.parent(resp)
	 *
	 * @param		{String} Response
	 */
	queryResponse: function() {
		this.fireEvent('onComplete', [this.element, this.request, this.response]);
	}

});

Autocompleter.Ajax.Json = new Class({

	Extends: Autocompleter.Ajax.Base,

	initialize: function(el, url, options) {
		this.parent(el, options);
		this.request = new Request.JSON($merge({
			'url': url,
			'link': 'cancel'
		}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
	},

	queryResponse: function(response) {
		this.parent();
		this.update(response);
	}

});

Autocompleter.Ajax.Xhtml = new Class({

	Extends: Autocompleter.Ajax.Base,

	initialize: function(el, url, options) {
		this.parent(el, options);
		this.request = new Request.HTML($merge({
			'url': url,
			'link': 'cancel',
			'update': this.choices
		}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
	},

	queryResponse: function(tree, elements) {
		this.parent();
		if (!elements || !elements.length) {
			this.hideChoices();
		} else {
			this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice || function(choice) {
				var value = choice.innerHTML;
				choice.inputValue = value;
				this.addChoiceEvents(choice.set('html', this.markQueryValue(value)));
			}, this);
			this.showChoices();
		}

	}

});


/**
 * Observer - Observe formelements for changes
 *
 * @version		1.0rc3
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */
var Observer = new Class({

	Implements: [Options, Events],

	options: {
		periodical: false,
		delay: 1000
	},

	initialize: function(el, onFired, options){
		this.setOptions(options);
		this.addEvent('onFired', onFired);
		this.element = $(el) || $$(el);
		/* Clientcide change */
		this.boundChange = this.changed.bind(this);
		this.resume();
	},

	changed: function() {
		var value = this.element.get('value');
		if ($equals(this.value, value)) return;
		this.clear();
		this.value = value;
		this.timeout = this.onFired.delay(this.options.delay, this);
	},

	setValue: function(value) {
		this.value = value;
		this.element.set('value', value);
		return this.clear();
	},

	onFired: function() {
		this.fireEvent('onFired', [this.value, this.element]);
	},

	clear: function() {
		$clear(this.timeout || null);
		return this;
	},
	/* Clientcide change */
	pause: function(){
		$clear(this.timeout);
		$clear(this.timer);
		this.element.removeEvent('keyup', this.boundChange);
		return this;
	},
	resume: function(){
		this.value = this.element.get('value');
		if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this);
		else this.element.addEvent('keyup', this.boundChange);
		return this;
	}

});

var $equals = function(obj1, obj2) {
	return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2));
};
Element.implement({getCaretPosition:function(){if(this.createTextRange){var r=document.selection.createRange().duplicate();r.moveEnd('character',this.value.length);if(r.text==='')return this.value.length;return this.value.lastIndexOf(r.text)}else return this.selectionStart}});var ResizableTextbox=new Class({Implements:Options,options:{min:5,max:500,step:7},initialize:function(b,c){var d=this;this.setOptions(c);this.el=$(b);this.width=this.el.offsetWidth;this.el.addEvents({'keydown':function(){this.store('rt-value',this.get('value').length)},'keyup':function(){var a=d.options.step*this.get('value').length;if(a<=d.options.min)a=d.width;if(!(this.get('value').length==this.retrieve('rt-value')||a<=d.options.min||a>=d.options.max))this.setStyle('width',a)}})}});var TextboxList=new Class({Implements:[Events,Options],options:{resizable:{},className:'bit',separator:'###',extrainputs:true,startinput:true,hideempty:true,autoTag:false,autoTagSeparator:32},initialize:function(a,b){this.setOptions(b);this.element=$(a).setStyle('display','none');this.bits=new Hash;this.events=new Hash;this.count=0;this.current=false;this.maininput=this.createInput({'class':'maininput',name:'maininput',id:'maininput'});this.holder=new Element('ul',{'class':'holder','events':{'click':function(e){e=new Event(e).stop();if(this.maininput!=this.current)this.focus(this.maininput)}.bind(this)}}).inject(this.element,'before').adopt(this.maininput);this.makeResizable(this.maininput);this.setEvents()},setEvents:function(){document.addEvent(Browser.Engine.trident?'keydown':'keypress',function(e){if(!this.current)return;if(this.current.retrieve('type')=='box'&&e.code==Event.Keys.backspace)new Event(e).stop()}.bind(this));document.addEvents({'keyup':function(e){e=new Event(e).stop();if(!this.current)return;switch(e.code){case Event.Keys.left:return this.move('left');case Event.Keys.right:return this.move('right');case Event.Keys.backspace:return this.moveDispose();case this.options.autoTagSeparator:return this.autoTag()}}.bind(this),'click':function(){this.fireEvent('onBlur').blur()}.bind(this)})},update:function(){this.element.set('value',this.bits.getValues().join(this.options.separator));return this},add:function(a,b){var c=this.options.className+'-'+this.count++;var d=this.createBox($pick(b,a),{'id':c}).inject(this.current||this.maininput,'before');d.addEvent('click',function(e){e=new Event(e).stop();this.focus(d)}.bind(this));this.bits.set(c,a);if(this.options.extrainputs&&(this.options.startinput||d.getPrevious()))this.addSmallInput(d,'before');return d},addSmallInput:function(a,b){var c=this.createInput({'class':'smallinput'}).inject(a,b);c.store('small',true);this.makeResizable(c);if(this.options.hideempty)c.setStyle('display','none');return c},dispose:function(a){this.bits.erase(a.id);var b=a.getPrevious();if(b&&b.retrieve('small'))b.destroy();if(this.current==a)this.focus(a.getNext());if(a.retrieve('type')=='box')this.fireEvent('onBoxDispose',a);a.destroy();return this},focus:function(a,b){if(!this.current)this.fireEvent('onFocus',a);else if(this.current==a)return this;this.blur();a.addClass(this.options.className+'-'+a.retrieve('type')+'-focus');if(a.retrieve('small'))a.setStyle('display','block');if(a.retrieve('type')=='input'){this.fireEvent('onInputFocus',a);if(!b)this.callEvent(a.retrieve('input'),'focus')}else this.fireEvent('onBoxFocus',a);this.current=a;return this},blur:function(a){if(!this.current)return this;if(this.current.retrieve('type')=='input'){var b=this.current.retrieve('input');if(!a)this.callEvent(b,'blur');this.fireEvent('onInputBlur',b)}else this.fireEvent('onBoxBlur',this.current);if(this.current.retrieve('small')&&!b.get('value')&&this.options.hideempty)this.current.setStyle('display','none');this.current.removeClass(this.options.className+'-'+this.current.retrieve('type')+'-focus');this.current=false;if(this.options.autoTag)this.autoTag();return this},createBox:function(a,b){return new Element('li',$extend(b,{'class':this.options.className+'-box'})).set('html',a).store('type','box')},createInput:function(a){var b=new Element('li',{'class':this.options.className+'-input'});var c=new Element('input',$extend(a,{'type':'text','events':{'click':function(e){e=new Event(e).stop()},'focus':function(e){if(!this.isSelfEvent('focus'))this.focus(b,true)}.bind(this),'blur':function(){if(!this.isSelfEvent('blur'))this.blur(true)}.bind(this),'keydown':function(e){this.store('lastvalue',this.value).store('lastcaret',this.getCaretPosition())}}}));return b.store('type','input').store('input',c).adopt(c)},callEvent:function(a,b){this.events.set(b,a);a[b]()},isSelfEvent:function(a){return(this.events.get(a))?!!this.events.erase(a):false},makeResizable:function(a){var b=a.retrieve('input');b.store('resizable',new ResizableTextbox(b,$extend(this.options.resizable,{min:b.offsetWidth,max:this.element.getStyle('width').toInt()})));return this},checkInput:function(){var a=this.current.retrieve('input');return(!a.retrieve('lastvalue')||(a.getCaretPosition()===0&&a.retrieve('lastcaret')===0))},move:function(a){var b=this.current['get'+(a=='left'?'Previous':'Next')]();if(b&&(!this.current.retrieve('input')||((this.checkInput()||a=='right'))))this.focus(b);return this},moveDispose:function(){if(this.current.retrieve('type')=='box')return this.dispose(this.current);if(this.checkInput()&&this.bits.getKeys().length&&this.current.getPrevious())return this.focus(this.current.getPrevious())},autoTag:function(){if(!this.autoTag)return this;var a=this.maininput.getChildren('input');if(!a.length)return this;var b=a[0].get('value');if(!b.length)return this;while(b.substr(b.length-1,b.length)==String.fromCharCode(this.options.autoTagSeparator))b=b.substr(0,b.length-1);if(!b.length)return this;this.add(b);a[0].set('value','');return this}});var FacebookList=new Class({Extends:TextboxList,createBox:function(a,b){var c=this.parent(a,b);c.addEvents({'mouseenter':function(){this.addClass('bit-hover')},'mouseleave':function(){this.removeClass('bit-hover')}});c.adopt(new Element('a',{'href':'#','class':'closebutton','events':{'click':function(e){new Event(e).stop();if(!this.current)this.focus(this.maininput);this.dispose(c)}.bind(this)}}));return c}});

/*
	Class: FormCheck
		Performs different tests on forms and indicates errors.
		
	Usage:
		Works with these types of fields :
		- input (text, radio, checkbox)
		- textarea
		- select
		
		You just need to add a specific class to each fields you want to check. 
		For example, if you add the class
			(code)
			validate['required','length[4, -1]','differs[email]','digit']
			(end code)
		the value's field must be set (required) with a minimum length of four chars (4, -1), 
		must differs of the input named email (differs[email]), and must be digit. 
		
		You can perform check during the datas entry or on the submit action, shows errors as tips or in a div before or after the field, 
		show errors one by one or all together, show a list of all errors at the top of the form, localize error messages, add new regex check, ...
		
		The layout is design only with css. Now I added a hack to use transparent png with IE6, so you can use png images in formcheck.css (works only for theme, so the file must be named formcheck.css). It can also works with multiple forms on a single html page.
		The class supports now internationalization. To use it, simply specify a new <script> element in your html head, like this : <script type="text/javascript" src="formcheck/lang/fr.js"></script>.

		If you add the class
			(code)
			validate['submit']
			(end code)
		to an element like an anchor (or anything else), this element will act as a submit button.
		
		N.B. : you must load the language script before the formcheck and this method overpass the old way. You can create new languages following existing ones. You can otherwise still specifiy the alerts' strings when you initialize the Class, with options.
		If you don't use a language script, the alert will be displayed in english.
	
	Test type:
		You can perform various test on fields by addind them to the validate class. Be careful to *not use space chars*. Here is the list of them.
			
		required 					- The field becomes required. This is a regex, you can change it with class options.
		alpha 						- The value is restricted to alphabetic chars. This is a regex, you can change it with class options.
		alphanum 					- The value is restricted to alphanumeric characters only. This is a regex, you can change it with class options.
		nodigit 					- The field doesn't accept digit chars. This is a regex, you can change it with class options.
		digit 						- The value is restricted to digit (no floating point number) chars, you can pass two arguments (f.e. digit[21,65]) to limit the number between them. Use -1 as second argument to not set a maximum.
		number 						- The value is restricted to number, including floating point number. This is a regex, you can change it with class options.
		email 						- The value is restricted to valid email. This is a regex, you can change it with class options.
		phone 						- The value is restricted to phone chars. This is a regex, you can change it with class options.
		url: 						- The value is restricted to url. This is a regex, you can change it with class options.
		confirm 					- The value has to be the same as the one passed in argument. f.e. confirm[password].
		differs 					- The value has to be diferent as the one passed in argument. f.e. differs[user].
		length 						- The value length is restricted by argument (f.e. length[6,10]). Use -1 as second argument to not set a maximum.
		
	Parameters:
		When you initialize the class with addEvent, you can set some options. If you want to modify regex, you must do it in a hash, like for display or alert. You can also add new regex check method by adding the regex and an alert with the same name.
		
		Required:
			
			form_id - The id of the formular. This is required.
			
		Optional:
			
			submitByAjax 			- you can set this to true if you want to submit your form with ajax. You should use provided events to handle the ajax request (see below). By default it is false.
			ajaxResponseDiv 		- id of element to inject ajax response into (can also use onAjaxSuccess). By default it is false.
			ajaxEvalScripts 		- use evalScripts in the Request response. Can be true or false, by default it is false.
			onAjaxRequest 			- Function to fire when the Request event starts.
			onAjaxSuccess 			- Function to fire when the Request receives .  Args: response [the request response] - see Mootools docs for Request.onSuccess.
			onAjaxFailure 			- Function to fire if the Request fails.
			
			tipsClass 				- The class to apply to tipboxes' errors. By default it is 'fc-tbx'.
			errorClass 				- The class to apply to alertbox (not tips). By default it is 'fc-error'.
			fieldErrorClass 		- The class to apply to fields with errors, except for radios. You should also turn on  options.addClassErrorToField. By default it is 'fc-field-error'
		
		Display:
			This is a hash of display settings. in here you can modify.
			
			showErrors 				- 0 : onSubmit, 1 : onSubmit & onBlur, by default it is 1.
			errorsLocation 			- 1 : tips, 2 : before, 3 : after, by default it is 1.
			indicateErrors 			- 0 : none, 1 : one by one, 2 : all, by default it is 1.
			keepFocusOnError 		- 0 : normal behaviour, 1 : the current field keep the focus as it remain errors. By default it is 0.
			checkValueIfEmpty 		- 0 : When you leave a field and you have set the showErrors option to 1, the value is tested only if a value has been set. 1 : The value is tested  in any case.  By default it is 1.
			addClassErrorToField 	- 0 : no class is added to the field, 1 : the options.fieldErrorClass is added to the field with an error (except for radio). By default it is 0.

			fixPngForIe 			- 0 : do nothing, 1 : fix png alpha for IE6 in formcheck.css. By default it is 1.
			replaceTipsEffect 		- 0 : No effect on tips replace when we resize the broswer, 1: tween transition on browser resize;
			closeTipsButton 		- 0 : the close button of the tipbox is hidden, 1 : the close button of the tipbox is visible. By default it is 1.
			flashTips 				- 0 : normal behaviour, 1 : the tipbox "flash" (disappear and reappear) if errors remain when the form is submitted. By default it is 0.
			tipsPosition 			- 'right' : the tips box is placed on the right part of the field, 'left' to place it on the left part. By default it is 'right'.
			tipsOffsetX 			- Horizontal position of the tips box (margin-left), , by default it is 100 (px).
			tipsOffsetY				- Vertical position of the tips box (margin-bottom), , by default it is -10 (px).
			
			listErrorsAtTop 		- List all errors at the top of the form, , by default it is false.
			scrollToFirst 			- Smooth scroll the page to first error and focus on it, by default it is true.
			fadeDuration 			- Transition duration (in ms), by default it is 300.
		
		Alerts:
			This is a hash of alerts settings. in here you can modify strings to localize or wathever else. %0 and %1 represent the argument.
			
			required 				- "This field is required."
			alpha 					- "This field accepts alphabetic characters only."
			alphanum 				- "This field accepts alphanumeric characters only."
			nodigit 				- "No digits are accepted."
			digit 					- "Please enter a valid integer."
			digitmin 				- "The number must be at least %0"
			digitltd 				- "The value must be between %0 and %1"
			number 					- "Please enter a valid number."
			email 					- "Please enter a valid email: <br /><span>E.g. yourname@domain.com</span>"
			phone 					- "Please enter a valid phone."
			url 					- "Please enter a valid url: <br /><span>E.g. http://www.domain.com</span>"
			confirm 				- "This field is different from %0"
			differs 				- "This value must be different of %0"
			length_str 				- "The length is incorrect, it must be between %0 and %1"
			length_fix 				- "The length is incorrect, it must be exactly %0 characters"
			lengthmax 				- "The length is incorrect, it must be at max %0"
			lengthmin 				- "The length is incorrect, it must be at least %0"
			checkbox 				- "Please check the box"
			radios 					- "Please select a radio"
			select 					- "Please choose a value"
		
	Example:
		You can initialize a formcheck (no scroll, custom classes and alert) by adding for example this in your html head this code :
		
		(code)
		<script type="text/javascript">
			window.addEvent('domready', function() {
				var myCheck = new FormCheck('form_id', {
					tipsClass : 'tips_box',
					display : {
						scrollToFirst : false
					},
					alerts : {
						required : 'This field is ablolutely required! Please enter a value'
					}
				})
			});
		</script>
		(end code)
	
	About:
		formcheck.js v.1.4rc5 for mootools v1.2 - 09 / 2008
		
		by Floor SA (http://www.floor.ch) MIT-style license
		
		Created by Luca Pillonel, last modified by Luca Pillonel
		
	Credits:
		This class was inspired by fValidator by Fabio Zendhi Nagao (http://zend.lojcomm.com.br)	
*/

var FormCheck = new Class({
	
	Implements: [Options, Events],

	options : {
		
		tipsClass: 'fc-tbx',				//tips error class
		errorClass: 'fc-error',				//div error class
		fieldErrorClass: 'fc-field-error',	//error class for elements
		
		submitByAjax : false,				//false : standard submit way, true : submit by ajax
		ajaxResponseDiv : false,			//element to inject ajax response into (can also use onAjaxSuccess) [cronix] 
		ajaxEvalScripts : false,			//use evalScripts in the Request response [cronix] 
		onAjaxRequest : $empty,				//Function to fire when the Request event starts 
		onAjaxSuccess : $empty,				//Function to fire when the Request receives .  Args: response [the request response] - see Mootools docs for Request.onSuccess 
		onAjaxFailure : $empty,				//Function to fire if the Request fails 
		
		display : {
			showErrors : 1,
			errorsLocation : 1,
			indicateErrors : 1,
			keepFocusOnError : 0,
			checkValueIfEmpty : 1,
			addClassErrorToField : 0,
			fixPngForIe : 1,
			replaceTipsEffect : 1,
			flashTips : 0,
			closeTipsButton : 1,
			tipsPosition : "right",
			tipsOffsetX : -45,
			tipsOffsetY : 0,
			listErrorsAtTop : false,
			scrollToFirst : true,
			fadeDuration : 300
		},
		
		alerts : {
			required: "This field is required.",
			alpha: "This field accepts alphabetic characters only.",
			alphanum: "This field accepts alphanumeric characters only.",
			nodigit: "No digits are accepted.",
			digit: "Please enter a valid integer.",
			digitltd: "The value must be between %0 and %1",
			number: "Please enter a valid number.",
			email: "Please enter a valid email.",
			phone: "Please enter a valid phone.",
			url: "Please enter a valid url.",
			
			confirm: "This field is different from %0",
			differs: "This value must be different of %0",
			length_str: "The length is incorrect, it must be between %0 and %1",
			length_fix: "The length is incorrect, it must be exactly %0 characters",
			lengthmax: "The length is incorrect, it must be at max %0",
			lengthmin: "The length is incorrect, it must be at least %0",
			checkbox: "Please check the box",
			radios: "Please select a radio",
			select: "Please choose a value"
		},
		
		regexp : {
			required : /[^.*]/,
			alpha : /^[a-z ._-]+$/i,
			alphanum : /^[a-z0-9 ._-]+$/i,
			digit : /^[-+]?[0-9]+$/,
			nodigit : /^[^0-9]+$/,
			number : /^[-+]?\d*\.?\d+$/,
			email : /^[a-z0-9._%-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i,
			phone : /^[\d\s ().-]+$/,
			url : /^(http|https|ftp)\:\/\/[a-z0-9\-\.]+\.[a-z]{2,3}(:[a-z0-9]*)?\/?([a-z0-9\-\._\?\,\'\/\\\+&amp;%\$#\=~])*$/i
		}
	},
	
	/*
	Constructor: initialize
		Constructor
	
		Add event on formular and perform some stuff, you now, like settings, ...
	*/
	initialize : function(form, options) {
		if (this.form = $(form)) {
			this.form.isValid = true;
			this.regex = ['length'];
			this.setOptions(options);
			
			//internalization
			if (typeof(formcheckLanguage) != 'undefined') this.options.alerts = formcheckLanguage;
			
			this.validations = [];
			this.alreadyIndicated = false;
			this.firstError = false;
			
			var regex = new Hash(this.options.regexp);
			regex.each(function(el, key) {
				this.regex.push(key);
			}, this);
	
			this.form.getElements("*[class*=validate]").each(function(el) {
				el.validation = [];
				var classes = el.getProperty("class").split(' ');
				classes.each(function(classX) {
					if(classX.match(/^validate(\[.+\])$/)) {
						var validators = eval(classX.match(/^validate(\[.+\])$/)[1]);
						for(var i = 0; i < validators.length; i++) {
							el.validation.push(validators[i]);
						}
						this.register(el);
					}
				}, this);
			}, this);
			
			this.form.addEvents({
				"submit": this.onSubmit.bind(this)
			});
			
			if(this.options.display.fixPngForIe) this.fixIeStuffs();
			document.addEvent('mousewheel', function(){
				this.isScrolling = false;
			}.bind(this));
		}
	},
	
	/*
	Function: register
		Private method
		
		Add listener on fields
	*/
	register : function(el) {
		this.validations.push(el);
		el.errors = [];
		if (el.validation[0] == 'submit') {
			el.addEvent('click', function(e){
				this.onSubmit(e);
			}.bind(this));
			return true;
		}

		if (this.isChildType(el) == false) el.addEvent('blur', function(e) {
			if((el.element || this.options.display.showErrors == 1) && (this.options.display.checkValueIfEmpty || el.value)) this.manageError(el, 'blur');
		}.bind(this))
		//We manage errors on radio
		else if (this.isChildType(el) == true) {
			//We get all radio from the same group and add a blur option
			var nlButtonGroup = this.form.getElements('input[name="'+ el.getProperty("name") +'"]');
			nlButtonGroup.each(function(radio){
				radio.addEvent('blur', function(){
					if((el.element || this.options.display.showErrors == 1) && (this.options.display.checkValueIfEmpty || el.value)) this.manageError(el, 'click');
				}.bind(this))
			},this);
		}
	},
	
	/*
	Function: validate
		Private method
		
		Dispatch check to other methods
	*/
	validate : function(el) {
		el.errors = [];
		el.isOk = true;
		//On valide l'lment qui n'est pas un radio ni checkbox
		el.validation.each(function(rule) {
			if(this.isChildType(el)) {
				if (this.validateGroup(el) == false) {
					el.isOk = false;
				}
			} else {
				var ruleArgs = [];
				if(rule.match(/^.+\[/)) {
					var ruleMethod = rule.split('[')[0];
					ruleArgs = eval(rule.match(/^.+(\[.+\])$/)[1].replace(/([A-Z\.]+)/i, "'$1'"));
				} else var ruleMethod = rule;
				
				if (this.regex.contains(ruleMethod) && el.get('tag') != "select") {
					if (this.validateRegex(el, ruleMethod, ruleArgs) == false) {
						el.isOk = false;
					}
				}
				
				if (ruleMethod == 'confirm') {
					if (this.validateConfirm(el, ruleArgs) == false) {
						el.isOk = false;
					}
				}
				if (ruleMethod == 'differs') {
					if (this.validateDiffers(el, ruleArgs) == false) {
						el.isOk = false;
					}
				}
				if (el.get('tag') == "select" || (el.type == "checkbox" && ruleMethod == 'required')) {
					if (this.simpleValidate(el) == false) {
						el.isOk = false;
					}
				}
			}
		}, this);
		
		if (el.isOk) return true;
		else return false;
	},
	
	/*
	Function: simpleValidate
		Private method
		
		Perform simple check for select fields and checkboxes
	*/
	simpleValidate : function(el) {
		if (el.get('tag') == 'select' && (el.options[el.selectedIndex].text == el.options[0].text)) {
			el.errors.push(this.options.alerts.select);
			return false;
		} else if (el.type == "checkbox" && el.checked == false) {
			el.errors.push(this.options.alerts.checkbox);
			return false;
		}
		return true;
	},
	
	/*
	Function: validateRegex
		Private method
		
		Perform regex validations
	*/
	validateRegex : function(el, ruleMethod, ruleArgs) {
		var msg = "";
		if (ruleArgs[1] && ruleMethod == 'length') {
			if (ruleArgs[1] == -1) {
				this.options.regexp.length = new RegExp("^[\\s\\S]{"+ ruleArgs[0] +",}$");
				msg = this.options.alerts.lengthmin.replace("%0",ruleArgs[0]);
			} else if(ruleArgs[0] == ruleArgs[1]) {
				this.options.regexp.length = new RegExp("^[\\s\\S]{"+ ruleArgs[0] +"}$");
				msg = this.options.alerts.length_fix.replace("%0",ruleArgs[0]);
			} else {
				this.options.regexp.length = new RegExp("^[\\s\\S]{"+ ruleArgs[0] +","+ ruleArgs[1] +"}$");
				msg = this.options.alerts.length_str.replace("%0",ruleArgs[0]).replace("%1",ruleArgs[1]);
			}
		} else if (ruleArgs[0] && ruleMethod == 'length') {
			this.options.regexp.length = new RegExp("^.{0,"+ ruleArgs[0] +"}$");
			msg = this.options.alerts.lengthmax.replace("%0",ruleArgs[0]);
		} else {
			msg = this.options.alerts[ruleMethod];
		}
		if (ruleArgs[1] && ruleMethod == 'digit') {
			var regres = true;
			if (!this.options.regexp.digit.test(el.value)) {
				el.errors.push(this.options.alerts[ruleMethod]);
				regres = false;
			}
			if (ruleArgs[1] == -1) {
				if (el.value >= ruleArgs[0]) var valueres = true; else var valueres = false;
				msg = this.options.alerts.digitmin.replace("%0",ruleArgs[0]);
			} else {
				if (el.value >= ruleArgs[0] && el.value <= ruleArgs[1]) var valueres = true; else var valueres = false;
				msg = this.options.alerts.digitltd.replace("%0",ruleArgs[0]).replace("%1",ruleArgs[1]);
			}
			if (regres == false || valueres == false) {
				el.errors.push(msg);
				return false;
			}
		} else if (this.options.regexp[ruleMethod].test(el.value) == false)  {
			el.errors.push(msg);
			return false;
		}
		return true;
	},

	/*
	Function: validateConfirm
		Private method
		
		Perform confirm validations
	*/
	validateConfirm: function(el,ruleArgs) {
		if (el.validation.contains('required') == false) {
			el.validation.push('required');
		}
		var confirm = ruleArgs[0];
		if(el.value != this.form[confirm].value){
			msg = this.options.alerts.confirm.replace("%0",ruleArgs[0]);
			el.errors.push(msg);
			return false;
		}
		return true;
	},
	
	/*
	Function: validateDiffers
		Private method
		
		Perform differs validations
	*/
	validateDiffers: function(el,ruleArgs) {
		var confirm = ruleArgs[0];
		if(el.value == this.form[confirm].value){
			msg = this.options.alerts.differs.replace("%0",ruleArgs[0]);
			el.errors.push(msg);
			return false;
		}
		return true;
	},
	
	/*
	Function: isChildType
		Private method
		
		Determine if the field is a group of radio or not.
	*/
	isChildType: function(el) {
		var elType = el.type.toLowerCase();
		if((elType == "radio")) return true;
		return false;
	},
	
	/*
	Function: validateGroup
		Private method
		
		Perform radios validations
	*/
	validateGroup : function(el) {
		el.errors = [];
		var nlButtonGroup = this.form[el.getProperty("name")];
		el.group = nlButtonGroup;
		var cbCheckeds = false;
		
		for(var i = 0; i < nlButtonGroup.length; i++) {
			if(nlButtonGroup[i].checked) {
				cbCheckeds = true;
			}
		}
		if(cbCheckeds == false) {
			el.errors.push(this.options.alerts.radios);
			return false;
		} else {
			return true;	
		}
	},
	
	/*
	Function: listErrorsAtTop
		Private method
		
		Display errors
	*/
	listErrorsAtTop : function(obj) {
		if(!this.form.element) {
			 this.form.element = new Element('div', {'id' : 'errorlist', 'class' : this.options.errorClass}).injectTop(this.form);
		}
		if ($type(obj) == 'collection') {
			new Element('p').set('html',"<span>" + obj[0].name + " : </span>" + obj[0].errors[0]).injectInside(this.form.element);
		} else {
			if ((obj.validation.contains('required') && obj.errors.length > 0) || (obj.errors.length > 0 && obj.value && obj.validation.contains('required') == false)) {
				obj.errors.each(function(error) {
					new Element('p').set('html',"<span>" + obj.name + " : </span>" + error).injectInside(this.form.element);
				}, this);
			}
		}
	},
	
	/*
	Function: manageError
		Private method
		
		Manage display of errors boxes
	*/
	manageError : function(el, method) {
		var isValid = this.validate(el);
		if ((!isValid && el.validation.contains('required')) || (!el.validation.contains('required') && el.value && !isValid)) {
			if(this.options.display.listErrorsAtTop == true && method == 'submit')
				this.listErrorsAtTop(el, method);
			if (this.options.display.indicateErrors == 2 ||this.alreadyIndicated == false || el.name == this.alreadyIndicated.name)
			{
				if(!this.firstError) this.firstError = el;
				this.alreadyIndicated = el;
				if (this.options.display.keepFocusOnError && el.name == this.firstError.name) (function(){el.focus()}).delay(20);
				this.addError(el);
				return false;
			}
		} else if ((isValid || (!el.validation.contains('required') && !el.value)) && el.element) {
			this.removeError(el);
			return true;
		}
		return true;
	},
	
	/*
	Function: addError
		Private method
		
		Add error message
	*/
	addError : function(obj) {
		if(!obj.element && this.options.display.indicateErrors != 0) {
			if (this.options.display.errorsLocation == 1) {
				var pos = (this.options.display.tipsPosition == 'left') ? obj.getCoordinates().left : obj.getCoordinates().right;
				var options = {
					'opacity' : 0,
					'position' : 'absolute',
					'float' : 'left',
					'left' : pos + this.options.display.tipsOffsetX
				}
				obj.element = new Element('div', {'class' : this.options.tipsClass, 'styles' : options}).injectInside(document.body);
				this.addPositionEvent(obj);
			} else if (this.options.display.errorsLocation == 2){
				obj.element = new Element('div', {'class' : this.options.errorClass, 'styles' : {'opacity' : 0}}).injectBefore(obj);
			} else if (this.options.display.errorsLocation == 3){
				obj.element = new Element('div', {'class' : this.options.errorClass, 'styles' : {'opacity' : 0}});
				if ($type(obj.group) == 'object' || $type(obj.group) == 'collection')
					obj.element.injectAfter(obj.group[obj.group.length-1]);
				else
					obj.element.injectAfter(obj);
			}
		}					
		if (obj.element) {
			obj.element.empty();
			if (this.options.display.errorsLocation == 1) {
				var errors = [];
				obj.errors.each(function(error) {
					errors.push(new Element('p').set('html', error));
				});
				var tips = this.makeTips(errors).injectInside(obj.element);
				if(this.options.display.closeTipsButton) {
					tips.getElements('a.close').addEvent('mouseup', function(){
						this.removeError(obj);
					}.bind(this));
				}
				obj.element.setStyle('top', obj.getCoordinates().top - tips.getCoordinates().height + this.options.display.tipsOffsetY);
			} else {
				obj.errors.each(function(error) {
					new Element('p').set('html',error).injectInside(obj.element);
				});
			}
			
			if (!Browser.Engine.trident5 && obj.element.getStyle('opacity') == 0)
				new Fx.Morph(obj.element, {'duration' : this.options.display.fadeDuration}).start({'opacity':[1]});
			else
				obj.element.setStyle('opacity', 1);
		}
		if (this.options.display.addClassErrorToField && this.isChildType(obj) == false)
		{
			obj.addClass(this.options.fieldErrorClass);
		}
	},
	
	/*
	Function: addPositionEvent
		
		Update tips position after a browser resize
	*/
	addPositionEvent : function(obj) {
		if(this.options.display.replaceTipsEffect) {
			obj.event = function(){
				new Fx.Morph(obj.element, {
					'duration' : this.options.display.fadeDuration
				}).start({ 
					'left':[obj.element.getStyle('left'), obj.getCoordinates().right + this.options.display.tipsOffsetX],
					'top':[obj.element.getStyle('top'), obj.getCoordinates().top - obj.element.getCoordinates().height + this.options.display.tipsOffsetY]
				});
			}.bind(this);
			
		} else {
			obj.event = function(){
				obj.element.setStyles({ 
					'left':obj.getCoordinates().right + this.options.display.tipsOffsetX,
					'top':obj.getCoordinates().top - obj.element.getCoordinates().height + this.options.display.tipsOffsetY
				});
			}.bind(this)
		}
		window.addEvent('resize', obj.event);
	},
	
	/*
	Function: removeError
		Private method
		
		Remove the error display
	*/
	removeError : function(obj) {
		this.firstError = false;
		this.alreadyIndicated = false;
		obj.errors = [];
		obj.isOK = true;
		window.removeEvent('resize', obj.event);
		if (this.options.display.errorsLocation == 2)
			new Fx.Morph(obj.element, {'duration' : this.options.display.fadeDuration}).start({ 'height':[0] });
		if (!Browser.Engine.trident5) {
			new Fx.Morph(obj.element, {
				'duration' : this.options.display.fadeDuration,
				'onComplete' : function() {
					if (obj.element) {
						obj.element.destroy();
						obj.element = false;
					}
				}.bind(this)
			}).start({ 'opacity':[1,0] });
		} else {
			obj.element.destroy();
			obj.element = false;
		}
		
		if (this.options.display.addClassErrorToField && !this.isChildType(obj))
		{
			obj.removeClass(this.options.fieldErrorClass);
		}
	},
	
	/*
	Function: focusOnError
		Private method
		
		Create set the focus to the first field with an error if needed
	*/
	focusOnError : function (obj) {
		if (this.options.display.scrollToFirst && !this.alreadyFocused && !this.isScrolling) {
			if (this.alreadyIndicated.element) {
				switch (this.options.display.errorsLocation){
					case 1 : 
						var dest = obj.element.getCoordinates().top;
						break;
					case 2 :
						var dest = obj.element.getCoordinates().top-30;
						break;
					case 3 :
						var dest = obj.getCoordinates().top-30;
						break;
				}
				this.isScrolling = true;
			} else if (!this.options.display.indicateErrors) {
				var dest = obj.getCoordinates().top-30;
			}
			if (window.getScroll.y != dest) {
				new Fx.Scroll(window, {
					onComplete : function() {
						this.isScrolling = false;
						obj.focus();
					}.bind(this)
				}).start(0,dest);
			} else {
				this.isScrolling = false;
				obj.focus();
			}
			this.alreadyFocused = true;
		}
	},
	
	/*
	Function: fixIeStuffs
		Private method
		
		Fix png for IE6
	*/
	fixIeStuffs : function () {
		if (Browser.Engine.trident4) {
			//We fix png stuffs
			var rpng = new RegExp('url\\(([\.a-zA-Z0-9_/:-]+\.png)\\)');
			var search = new RegExp('(.+)formcheck\.css');
			for (var i = 0; i < document.styleSheets.length; i++){
				if (document.styleSheets[i].href.match(/formcheck\.css$/)) {
					var root = document.styleSheets[i].href.replace(search, '$1');
					var count = document.styleSheets[i].rules.length;
					for (var j = 0; j < count; j++){
						var cssstyle = document.styleSheets[i].rules[j].style;
						var bgimage = root + cssstyle.backgroundImage.replace(rpng, '$1');
						if (bgimage && bgimage.match(/\.png/i)){
							var scale = (cssstyle.backgroundRepeat == 'no-repeat') ? 'crop' : 'scale';
							cssstyle.filter =  'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, src=\'' + bgimage + '\', sizingMethod=\''+ scale +'\')';
							cssstyle.backgroundImage = "none";
						}
					}
				}
			}
		}
	},
	
	/*
	Function: makeTips
		Private method
		
		Create tips boxes
	*/
	makeTips : function(txt) {
		var table = new Element('table');
			table.cellPadding ='0';
			table.cellSpacing ='0';
			table.border ='0';
			
			var tbody = new Element('tbody').injectInside(table);
				var tr1 = new Element('tr').injectInside(tbody);
					new Element('td', {'class' : 'tl'}).injectInside(tr1);
					new Element('td', {'class' : 't'}).injectInside(tr1);
					new Element('td', {'class' : 'tr'}).injectInside(tr1);
				var tr2 = new Element('tr').injectInside(tbody);
					new Element('td', {'class' : 'l'}).injectInside(tr2);
					var cont = new Element('td', {'class' : 'c'}).injectInside(tr2);
						var errors = new Element('div', {'class' : 'err'}).injectInside(cont);
						txt.each(function(error) {
							error.injectInside(errors);
						});
						if (this.options.display.closeTipsButton) new Element('a',{'class' : 'close'}).injectInside(cont);
					//	new Element('div', {'style' : "clear:both"}).injectInside(cont);
					new Element('td', {'class' : 'r'}).injectInside(tr2);
				var tr3 = new Element('tr').injectInside(tbody);
					new Element('td', {'class' : 'bl'}).injectInside(tr3);
					new Element('td', {'class' : 'b'}).injectInside(tr3);
					new Element('td', {'class' : 'br'}).injectInside(tr3);			
		return table;
	},
	
	/*
	Function: reinitialize
		Private method		
		
		Reinitialize form before submit check
	*/
	reinitialize: function() {
		this.validations.each(function(el) {
			if (el.element) {
				el.errors = [];
				el.isOK = true;
				if(this.options.display.flashTips == 1) {
					el.element.destroy();
					el.element = false;
				}
			}
		}, this);
		if (this.form.element) this.form.element.empty();
		this.alreadyFocused = false;
		this.firstError = false;
		this.alreadyIndicated = false;
		this.form.isValid = true;
	},
	
	/*
	Function: submitByAjax
		Private method		
		
		Send the form by ajax, and replace the form with response
	*/
	
	submitByAjax: function() {
		var url = this.form.getProperty('action');
		this.fireEvent('ajaxRequest');
		new Request({
			url: url,
			method: this.form.getProperty('method'),
			data : this.form.toQueryString(),
			evalScripts: this.options.ajaxEvalScripts,
			onFailure: function(instance){
				this.fireEvent('ajaxFailure', instance);
			}.bind(this),
			onSuccess: function(result){
				this.fireEvent('ajaxSuccess', result);
				if(this.options.ajaxResponseDiv) $(this.options.ajaxResponseDiv).set('html',result);
			}.bind(this)
		}).send();
	},

	
	/*
	Function: onSubmit
		Private method		
		
		Perform check on submit action
	*/
	onSubmit: function(event) {
		new Event(event).stop();
		this.reinitialize();

		this.validations.each(function(el) {
			if(!this.manageError(el,'submit')) this.form.isValid = false;
		}, this);
		
		(this.form.isValid) ? (this.options.submitByAjax) ? this.submitByAjax() : this.form.submit() : this.focusOnError(this.firstError);
	}
});
/* ------------------------------------------------------------------------
 * common.js
 * 
 * Includes extensions to the mootools library.
 * SVN: $Id: common.js 4174 2009-02-06 19:35:43Z kwallsmith $
 * ------------------------------------------------------------------------ */

/**
 * Returns the URL fragment from the current element's href property.
 * 
 * @return String|null
 */
Element.Properties.fragment = { get: function() {
  var href = this.get('href');
  return href.indexOf('#') > -1 ? this.get('href').replace(/^.*#/, '') : null;
}};

/**
 * Mimics the send property, but uses Request.JSON.
 */
Element.Properties.sendJson = { get: function(options) {
  if (options || !this.retrieve('sendJson')) {
    if (options || !this.retrieve('send:options')) this.set('send', options);
    this.store('sendJson', new Request.JSON(this.retrieve('send:options')));
  }
  return this.retrieve('sendJson');
}};

Element.implement({

  /**
   * Sends the current element using Request.JSON.
   * 
   * @param  {Object} data To extend what's in the DOM
   * @return Element
   */
  sendJson: function(data) {
    var sender = this.get('sendJson');
    sender.send({ data: $extend(this.serialize(), data || { }) });
    return this;
  },

  /**
   * Serializes the current element as an object.
   * 
   * @return Object
   */
  serialize: function() {
    return this.toQueryString().parseQuery();
  },

  /**
   * Clones the current element as a ghost of itself.
   * 
   * @param  {Object} styles
   * @return Element The ghost clone
   */
  ghost: function(styles) {
    styles = $extend({
      position: 'absolute',
      opacity: 0.7,
      zIndex: 1000
    }, styles || { });

    var pos = this.getPosition();
    styles.left = pos.x;
    styles.top = pos.y;

    return this.clone().setStyles(styles).inject(document.body);
  },

  /**
   * Toggles the current element's display style based on a condition.
   * 
   * @param  {Boolean} condition
   * @return Element
   */
  toggleTo: function(condition) {
    return this[condition ? 'show' : 'hide']();
  },

  /**
   * Returns an array of the current element's siblings.
   * 
   * @return Array
   */
  getSiblings: function() {
    return this.getParent().getChildren().erase(this);
  },

  /**
   * Removes multiple classes from an element.
   * 
   * @param  {String} className
   * @return Element
   */
  removeClasses: function() {
    for (var i = 0, l = arguments.length; i < l; i++)
      this.removeClass(arguments[i]);
    return this;
  },

  /**
   * Adds a condition to the CNET method.
   * 
   * @param  {Object} options
   * @return Element
   */
  reveal: function(options) {
    if (!this.isVisible()) this.get('reveal', options).reveal();
    return this;
  },

  /**
   * Adds a condition to the CNET method.
   * 
   * @param  {Object} options
   * @return Element
   */
  dissolve: function(options) {
    if (this.isVisible()) this.get('reveal', options).dissolve();
    return this;
  },

  /**
   * Returns the y-coordinate for the bottom of the element.
   * 
   * @return Number
   */
  getBottom: function() {
    return this.getCoordinates().bottom;
  },

  /**
   * A convenience method that also adds element specific offsets.
   * 
   * Options include:
   * 
   *  * offsetX
   *  * offsetY
   *  * title
   *  * text
   * 
   * @param  {Object} options
   * @return Element
   */
  addTip: function(options) {
    var that = this;

    options = $extend({
      offsetX: 0,
      offsetY: 0
    }, options || { });

    // singleton
    Tips.instance = Tips.instance || new Tips(null, { className: 'tip-container', fixed: true, offsets: { x: 0, y: 0 }});

    if (options.title)
      this.store('tip:title', options.title);
    if (options.text)
      this.store('tip:text', options.text);

    this.store('tip:enter', function(event) {
      Tips.prototype.elementEnter.call(Tips.instance, event, that);

      // adjust position of tip based on element properties
      if (options.offsetX)
        Tips.instance.tip.setStyle('left', Tips.instance.tip.getStyle('left').toInt() + options.offsetX);
      if (options.offsetY)
        Tips.instance.tip.setStyle('top', Tips.instance.tip.getStyle('top').toInt() + options.offsetY);
    });

    Tips.instance.attach(this);

    return this;
  }

});

Array.implement({

  /**
   * Returns the first item in the array for which the callback returns true.
   * 
   * @return mixed
   */
  find: function(fn, bind) {
    for (var i = 0, l = this.length; i < l; i++) if (fn.call(bind, this[i], i, this)) return this[i];
  }

});

Function.implement({

  /**
   * Wraps the current function in another. Copied from Prototype 1.6.0.2.
   * 
   * @param  {Function} wrapper
   * @return Function
   */
  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  }

});

Native.implement([Window, Document], {

  /**
   * Returns the y-coordinate at the bottom of the viewport.
   * 
   * @return Number
   */
  getScrollBottom: function() {
    return this.getScrollTop()+this.getHeight();
  }

});

/**
 * Parses a query string into an object.
 */
String.prototype.parseQuery = function(decodeKey, decodeValue) {
  var object = { };
  for (var query = this.split('&'), i = 0, l = query.length; i < l; i++) {
    var q = query[i].split('='),
      k = decodeKey ? decodeURIComponent(q[0]) : q[0],
      v = decodeValue ? decodeURIComponent(q[1]) : q[1];
    object[k] = object[k] ? $splat(object[k]).include(v) : v;
  }
  return object;
};

Dynamic = Dynamic || { };

/* ------------------------------------------------------------------------
 * Bound.js
 * 
 * A Class.Extra for binding an abstraction object to an element in the DOM.
 * SVN: $Id: Bound.js 3045 2008-10-24 07:26:57Z kwallsmith $
 * ------------------------------------------------------------------------ */

var Bound = {

  /**
   * @deprecated In favor of Model and Event.Bubbler
   * 
   * Binds the current object to an element.
   * 
   * If both the current object and its parent implement Events, the fireEvent
   * method will be wrapped to bubble events (requires a this.Name property).
   * 
   * Requires the following methods exist:
   * 
   *  * extractData()     Populates this.data with data painted to the DOM
   *  * createElement()   Creates an element using this.data
   * 
   * @param  {Element|Object} element Element or data object
   * @param  {Object}         parent  Parent abstraction object
   * @return Object
   */
  bind: function(element, parent) {
    var that = this;

    if ($(element)) {

      // argument is an element
      this.element = $(element);
      // this.data = this.extractData();

    } else {

      // argument is a json data object
      this.data = element;
      // this.element = this.createElement();

    }

    this.element.store('abstraction', this);
    this.parent = parent;

    if (this.parent && 'function' == $type(this.fireEvent) && 'function' == $type(this.parent.fireEvent)) {
      this.fireEvent = this.fireEvent.wrap(function(wrapped, type, args, delay) {

        dbug.log('Firing "{type}" event on "{name}" object'.substitute({ type: type, name: that.Name }));
        wrapped(type, args, delay);

        // add namespace
        type = that.Name && !type.contains(':') ? (that.Name+':'+type) : type;

        dbug.log('Firing "{type}" event on parent object'.substitute({ type: type }));
        that.parent.fireEvent(type, [that].concat(args), delay);

        return that;
      });
    }

    return this;
  }

};

/**
 * Represents a model object with a representation in the DOM.
 */
var Model = new Class({

  /**
   * @constructor
   */
  initialize: function(object) {
    var that = this;

    function setElement(element) {
      that.element = $(element);
      that.element.store('model', that);
    }

    if ('element' == $type(object) || 'string' == $type(object)) {
      setElement(object);
    } else {
      setElement(this.createElement(object));
    }
  },

  /**
   * Returns the current object's fields hash.
   * 
   * @return Hash
   */
  getFieldsHash: function() {
    this.fields = this.fields || new Hash();
    return this.fields;
  },

  /**
   * Adds a field and configures how to determine its value.
   * 
   * @param  {String} name
   * @param  {Array}  selectors An array of selectors or elements to use for coalescing a value
   * 
   * @return Model
   */
  addField: function(name, selectors) {
    this.getFieldsHash().set(name, $splat(selectors || []));
    return this;
  },

  /**
   * Returns the current value of the supplied field.
   * 
   * @param  {String} name
   * 
   * @return String
   */
  getValue: function(name) {
    var selectors = this.getFieldsHash().get(name), element;
    if ('array' == $type(selectors)) {
      for (var i = 0, l = selectors.length; i < l; i++) {
        var selector = selectors[i];

        if ('element' == $type(selector)) {
          element = selector;
          break;
        }

        if ('string' == $type(selector) && (element = this.element.getElement(selector))) {
          break;
        }
      }
      
      /* temp fix to ignore watermarks */
      /*
      if (!$(document.body).hasClass('explorer') && element && element.retrieve('watermark:title') == element.get('value')) {
        return null;
      }
      */
      return element ? element.get(element.match('*[name]') ? 'value' : 'text') : null;
    }
  },

  /**
   * Returns all values as represented in the DOM.
   * 
   * @return Object
   */
  getValues: function() {
    var that = this;
    return this.getFieldsHash().map(function(selectors, name) { return that.getValue(name); });
  },

  /**
   * Returns all values encoded as a JSON string.
   * 
   * @return String
   */
  getValuesAsJson: function() {
    return JSON.encode(this.getValues());
  },

  /**
   * Creates a DOM element using the current data object.
   * 
   * @param  {Object} data
   * 
   * @return Element
   */
  createElement: function(data) {
    var error = 'The method "Model.View#createElement()" must be overloaded.';
    dbug.log(error);
    throw error;
  }

});

/* ------------------------------------------------------------------------
 * Events.Bubbler.js
 * 
 * A Class.Extra to bubble events to a parent object.
 * SVN: $Id: Events.Bubbler.js 4174 2009-02-06 19:35:43Z kwallsmith $
 * ------------------------------------------------------------------------ */

Events.Bubbler = new Class({

  Extends: Events,

  /**
   * Specifies an object to bubble events "up" to.
   * 
   * @param  {Events} parent
   * @param  {String} namespace
   * 
   * @return Events.Bubbler
   */
  bubblesTo: function(parent, namespace) {
    this.bubblerParent = parent;
    this.bubblerNamespace = namespace;
    return this;
  },

  /**
   * Fires event on self first, then parent.
   * 
   * @see Events
   */
  fireEvent: function(type, args, delay) {
    this.parent(type, args, delay);

    if (this.bubblerParent && 'function' == $type(this.bubblerParent.fireEvent)) {
      if (this.bubblerNamespace && !type.contains(':')) {
        type = this.bubblerNamespace+':'+type;
        args = [this].concat(args);
      }

      dbug.log('bubbling {type} event'.substitute({ type: type }));
      this.bubblerParent.fireEvent(type, args);
    }

    return this;
  }

});

/**
 * A singleton modalbox instance.
 * 
 * Available options:
 * 
 *  * pleaseDestroy
 *  * locked
 * 
 * Example usage:
 * 
 *  <script>
 *    Modalbox.getInstance().show($('my_div'));
 *  </script>
 */
var Modalbox = new Class({

  Implements: [Options, Events],

  options: {
    pleaseDestroy: false,
    locked: false
  },

  /**
   * @constructor
   */
  initialize: function() {
    var that = this;

    function listenForKeyDown(event) {
      if ('esc' == event.key) that.hide();
    }

    this.window = new StickyWin.Fx.Modal({
      allowMultiple: true,
      content: '',
      fade: true,
      fadeDuration: 150,
      id: 'modalbox_wrapper',
      width: 480, 
      height: 385,
      modalOptions: {
        modalStyle: { /*backgroundColor: '#FFFFFF', opacity: 0.6*/ }
      },
      showNow: false,
      onDisplay: function() {
        that.element.getElements('form').each(function(form) { form.reset(); });
        Watermark.setup(that.element);
        window.addEvent('keydown', listenForKeyDown);
      },
      onClose: function() {
        that.empty();
        window.removeEvent('keydown', listenForKeyDown);
        that.fireEvent('hide');
      }
    });

    // set the window fx's link option
    this.window.fade(0);
    this.window.fadeFx.options.link = 'chain';

    this.element = this.window.win;
  },

  getCloseButton: function() {
    return new Element('a', {'class': 'close'}).addEvent('click', this.hide.bind(this));
  },
  
  /**
   * Updates and shows the modalbox.
   * 
   * @param  {Element} element
   * @param  {Object}  options
   * @return Modalbox
   */
  show: function(element, options) {
    var that = this;

    this.empty();

    this.removeEvents().setOptions($extend({
      pleaseDestroy: false,
      locked: false
    }, options || { }));

    this.window.modalOptions.hideOnClick = !this.options.locked;

    $A(this.element.childNodes).each(function(node){ Element.dispose(node); });
    this.element.adopt(element);
    that.options.locked || that.element.grab(that.getCloseButton());

    this.window.visible || this.window.position().show();

    this.fireEvent('show');

    return this;
  },

  /**
   * Hides the modalbox.
   * 
   * @return Modalbox
   */
  hide: function() {
    if (!this.options.locked) {
      this.window.hide();
    }
    return this;
  },

  /**
   * Empties the contents of the Modalbox.
   * 
   * @return Modalbox
   */
  empty: function() {
    this.options.pleaseDestroy && this.element.empty();
    this.options.pleaseDestroy = false;
    return this;
  }

});

Modalbox.extend({

  /**
   * Returns the Modalbox singleton.
   * 
   * @return Modalbox
   */
  getInstance: function() {
    Modalbox.instance = Modalbox.instance || new Modalbox();
    return Modalbox.instance;
  }

});

/* ------------------------------------------------------------------------
 * Bouncer.js
 * 
 * SVN: $Id: Bouncer.js 4197 2009-02-11 18:39:08Z kevin $
 * ------------------------------------------------------------------------ */

/**
 * Acts as a bouncer when authentication is required.
 * 
 * Assumes the current user is not authenticated if a login link is present
 * and automatically protects all links with a 'secure' class name.
 * 
 * Options
 * 
 *  * show:             Either 'login' 'password' or 'register'
 *  * leaving:          If true the modal window will not be removed upon authorization
 *  * locked:           Whether to allow cancelling
 *  * modalboxOptions:  Options for the modalbox
 * 
 * Events
 * 
 *  * authorize: Fired upon successful authentication
 */
var Bouncer = new Class({

  $family: { name: 'bouncer' },
  Implements: [Options, Events],

  options: {
    // onAuthorize: $empty,
    show: 'login',
    leaving: false,
    locked: false,
    modalboxOptions: { }
  },

  formErrors: [],

  /**
   * @constructor
   */
  initialize: function() {
    var that = this;

    this.bound = {
      request: this.request.bind(this),
      failure: this.failure.bind(this)
    };

    // element shortcuts
    this.element           = $('bouncer');
    this.userBlurb         = $('user_blurb');
    this.togglers          = this.element.getElements('.toggler');
    this.cancelers         = this.element.getElements('.cancel');
    this.secureLinks       = $$('a.secure');

    this.loginLink         = $('login_link');
    this.loginContainer    = $('bouncer_login_container');
    this.loginForm         = this.loginContainer.getElement('form');
    this.loginError        = this.loginContainer.getElement('.error');

    this.passwordLink      = this.element.getElement('a[href$="bouncer_password_container"]');
    this.passwordContainer = $('bouncer_password_container');
    this.passwordForm      = this.passwordContainer.getElement('form');
    this.passwordError     = this.passwordContainer.getElement('.error');

    this.registerLinks     = $$('a.register_link');
    this.registerContainer = $('bouncer_register_container');
    this.registerForm      = this.registerContainer.getElement('form');
    this.registerError     = this.registerContainer.getElement('.error');

    this.containers = [this.loginContainer, this.passwordContainer, this.registerContainer];
    this.forms = [this.loginForm, this.passwordForm, this.registerForm];

    if (this.userIsAnonymous()) {
      // user appears to be unauthorized
      this.loginLink.addEvent('click', function(event) {
        event.stop();
        that.open({ show: 'login' });
      });

      this.registerLinks.addEvent('click', function(event) {
        event.stop();
        that.open({ show: 'register' });
      });

      // protect all secure links
      this.secureLinks.addEvent('click', function(event) {
        if (that.userIsAnonymous()) {
          event.stop();
          that.open({
            show: 'login',
            leaving: true,
            onAuthorize: function() {
              window.location.href = event.target.href;
            }
          });
        }
      });
    }

    this.loginForm.set('send', {
      onSuccess: function(responseText) {

        Tracker.trackEvent('/login/success');

        // user is now authenticated
        that.fireEvent('authorize');
        if (!that.options.leaving) {
          // if we're staying here, freshen things up
          that.userBlurb.set('html', responseText);
          Modalbox.getInstance().hide();
        }
      },
      onRequest: this.bound.request,
      onFailure: function(xhr) {
        Tracker.trackEvent('/login/failure');
        that.bound.failure(xhr);
      }
    }).addEvent('submit', function(event) {
      event.stop();
      that.loginForm.send();
    });

    this.passwordLink.addEvent('click', function(event) {
      event.stop();
      that.show('password');
    });

    this.passwordForm.set('send', {
      onSuccess: function(responseText) {

        Tracker.trackEvent('/password/success');

        // an email has been sent
        that.loginForm.getElement('input[name*=username]').set('value', that.passwordForm.getElement('input[name*=username]').get('value'));
        that.show('login');
        that.loginError.set('text', 'Your password has been emailed to you.').highlight();
      },
      onRequest: this.bound.request,
      onFailure: function(xhr) {
        Tracker.trackEvent('/password/failure');
        that.bound.failure(xhr);
      }
    }).addEvent('submit', function(event) {
      event.stop();
      that.passwordForm.send();
    });

    this.registerForm.set('send', {
      onSuccess: function(responseText) {

        Tracker.trackEvent('/register/success');

        // user account is created, user is authenticated
        that.fireEvent('authorize');
        if (!that.options.leaving) {
          // if we're staying here, freshen things up
          that.userBlurb.set('html', responseText);
          Modalbox.getInstance().hide();
        }
      },
      onRequest: this.bound.request,
      onFailure: function(xhr) {
        Tracker.trackEvent('/register/failure');
        that.bound.failure(xhr);
      }
    }).addEvent('submit', function(event) {
      event.stop();
      that.registerForm.send();
    })

    this.togglers.addEvent('click', function(event) {
      event.stop();
      if (that.passwordContainer.isVisible()) {
        that.show('register');
      } else {
        that.resetForms();
        that.loginContainer.toggle();
        that.registerContainer.toggle();
      }
    });

    this.cancelers.addEvent('click', function(event) {
      event.stop();
      Modalbox.getInstance().hide();
    });
  },

  /**
   * Handles new requests.
   */
  request: function() {
    this.resetErrors();
  },

  /**
   * Handles form submission failure.
   */
  failure: function(xhr) {
    var form = this.getCurrentForm(), errors = JSON.decode(xhr.responseText);
    for (var name in errors) {
      var field = form.getElement('input[name$="['+name+']"]'),
        error = new Element('p', { text: errors[name], 'class': 'form-error' });
      field ? field.addClass('form-error').grab(error, 'after') : form.getElement('.error-container').grab(error);
      this.formErrors.push(error);
    }
  },

  /**
   * Returns the form element currently shown.
   * 
   * @return Element
   */
  getCurrentForm: function() {
    return this.containers.find(function(container) { return container.isVisible(); }).getElement('form');
  },

  /**
   * Returns true if the current user is not authenticated.
   * 
   * @return Boolean
   */
  userIsAnonymous: function() {
    return $('login_link') ? true : false;
  },

  /**
   * Shows the requested pane.
   * 
   * @param  {String} pane
   * @return Bouncer
   */
  show: function(pane) {

    Tracker.trackEvent('/'+pane);

    this.resetForms();
    this.loginContainer.toggleTo('login' == pane);
    this.passwordContainer.toggleTo('password' == pane);
    this.registerContainer.toggleTo('register' == pane);
    return this;
  },

  /**
   * Resets all forms.
   * 
   * @return Bouncer
   */
  resetForms: function() {
    this.forms.each(function(form) { form.reset(); });
    this.resetErrors();
    return this;
  },

  /**
   * Resets form errors.
   * 
   * @return Bouncer
   */
  resetErrors: function() {
    for (var i = 0, l = this.formErrors.length; i < l; i++) {
      this.formErrors[i].destroy();
    }
    this.formErrors = [];
    this.element.getElements('.form-error').removeClass('form-error');
    return this;
  },

  /**
   * Opens the authentication window.
   * 
   * @param  {Object} options
   * @return Bouncer
   */
  open: function(options) {
    this.removeEvents('authorize').setOptions(options);
    this.options.modalboxOptions.locked = this.options.locked;
    this.cancelers.toggleTo(!this.options.locked);
    this.show(this.options.show);
    Modalbox.getInstance().show(this.element.show(), this.options.modalboxOptions);
    return this;
  }

});

Bouncer.extend({

  /**
   * Get the Bouncer singleton.
   * 
   * @return Bouncer
   */
  getInstance: function() {
    Bouncer.instance = Bouncer.instance || new Bouncer();
    return Bouncer.instance;
  }

});

/**
 * Wrap the Request#send() method to handle 401 responses with the Bouncer.
 */
Request.prototype.send = Request.prototype.send.wrap(function(wrapped, options) {
  var that = this, onFailures = $unlink((this.$events || {}).failure || []);

  this.removeEvents('failure').addEvent('failure', function(xhr) {

    if (401 == xhr.status) {
      // present the login window, then try again onAuthorize
      dbug.log('Server returned 401 -- routing request through Bouncer');
      Bouncer.getInstance().open($extend({
        show: 'login',
        onAuthorize: function() { that.send(options); }
      }, that.options.bouncerOptions || { }));
    } else {
      onFailures.each(function(fn){ fn.call(that, xhr); });
    }

  });

  return wrapped(options);
});

/**
 * Uses input title attributes to create a watermark text that disappears on focus.
 * 
 * @todo Replace with clientcide OverText
 */
var Watermark = new Class({

  $family: { name: 'watermark' },

  /**
   * @constructor
   */
  initialize: function(element) {
    this.element = element.store('watermark:title', element.get('title')).erase('title');
    this.element.addEvents({
      focus: function(event) {
        var element = event.target;
        if (element.retrieve('watermark:title') == element.get('value'))
          $(element).set('value', '').removeClass('watermark');
      },
      blur: function(event) {
        var element = 'element' == $type(event) ? event : event.target;
        if (!element.get('value').trim()) {
          element.set('value', element.retrieve('watermark:title')).addClass('watermark');
        }
      }
    });
    this.element.get('value').trim() ? this.element.removeClass('watermark') : this.element.set('value', this.element.retrieve('watermark:title'));
  }
});

/* ------------------------------------------------------------------------ */

Watermark.extend({

  setup: function() {
    $(arguments[0] || document).getElements('.watermark').each(function(element) {
      element.retrieve('watermark:title') ? element.fireEvent('blur', element) : new Watermark(element);
    });
  },

  isEmpty: function(element) {
    var value = element.get('value');
    return !value || element.retrieve('watermark:title') == value;
  }

});

/**
 * Toggles the visibility of inline help messages.
 * 
 * Events:
 *  * show
 *  * hide
 */
var Helps = new Class({

  $family: { name: 'helps' },
  Implements: [Bound, Options, Events],

  options: {
    selector: '.help-item'
  },

  initialize: function(element, options) {
    var that = this;

    dbug.log("Initializing help");
    this.bind(element);
    this.setOptions(options);

    (this.hidden = Main.storage.get('helps_hidden')) ? this.hide(false) : this.show(false);

    this.element.addEvent('click', function(event) {
      event.stop();
      that.hidden ? that.show(true) : that.hide(true);
    });
  },

  show: function(turnOn) {
    // show dialogs, too
    dbug.log("Showing help");
    if (Main.helpDialogs) {
      Main.helpDialogs.show();
    }
    $$(this.options.selector).reveal({ duration: 'short' });
    if (turnOn) {
      this.element.set('text', 'on');
      Main.storage.set('helps_hidden', this.hidden = false);
    }
    this.fireEvent('show');
  },

  hide: function(turnOff) {
    // hide dialogs, too
    if (Main.helpDialogs) {
      Main.helpDialogs.hideDialog();
    }
    $$(this.options.selector).dissolve({ duration: 'short' });
    if (turnOff) {
      this.element.set('text', 'off');
      Main.storage.set('helps_hidden', this.hidden = true);
    }
    this.fireEvent('hide');
  }

});

/**
 * Controls display of dialogs w/ instrutional help text.
 * 
 * Events:
 *  * show
 *  * hide
 */
var HelpDialogs = new Class({

  $family: { name: 'help_dialogs' },
  Implements: [Events, Options],
  
  options : {
    selector: 'instruction_panel',
    maskSelector: 'instruction_panel_mask',
    closeSelector: 'instruction_panel_close',
    dialogIdAttr: 'panel-id'
  },

  initialize: function(options) {
    var that = this;

    this.setOptions(options);
    this.dialogId = $(this.options.selector).getAttribute(this.options.dialogIdAddr);
    this.storageToken = 'help_hidden_' + this.dialogId;
    
//    this.hidden = this.getHidden() ? this.hide() : this.show();
    
    $(this.options.closeSelector).addEvent('click', function(event) {
      event.stop();
      that.hide();
    });
    
  },

  getHidden: function() {
    //return Main.storage.get(this.storageToken);
    return false;
  },
  
  setHidden: function(val) {
    this.hidden = val;
//    Main.storage.set(this.storageToken, val);
  },
  
  show: function() {
    $(this.options.selector).reveal({ duration: 'short' });
    $(this.options.maskSelector).setStyle('display','block');
    this.setHidden(false);
    this.fireEvent('show');
  },

  hide: function() {
    // hide numbers
    if (Main.helps) {
      Main.helps.hide(false);
    }
  },
  
  hideDialog: function() {
    $(this.options.selector).dissolve({ duration: 'short' });
    $(this.options.maskSelector).setStyle('display','none');
    this.setHidden(true);
    this.fireEvent('hide');    
  }


});

var Widget = {

  place: function() {
    var elementId = Widget.elementId || 'widget', html, re = /\{(\w+)\}/g;
    $(elementId).getParent().set('html', html = Widget.embedTemplate.substitute({
      elementId: elementId,
      flashVars: $H(Widget.flashVars).toQueryString()
    }, re));
  }

};

Widget.embedTemplate = Dynamic.widgetEmbedTemplate;
Widget.flashVars = Dynamic.widgetFlashVars;
Widget.elementId = Dynamic.widgetElementId;

var Login = new Class({

  initialize: function() {
    Bouncer.getInstance().open({
      show: $('bouncer_show').get('value'),
      locked: true,
      leaving: true,
      onAuthorize: function() {
        window.location.href = $('on_authorize_url').get('value');
      }
    });
  }
  
});

/**
 * Analytics tracking for ListSpinner.com.
 *
 */

var Tracker = {

  constants : {
    LIST_PATH_PREFIX : "/lists"
  },

  actions : {
    /* key */           /* Description */       /* GA tag */
    'item-open'         : ['Open list item',    'listItem'],
    'play-video'        : ['Preview video',     'playClip'],
    'comment-add'       : ['Add comment',       'addComment'],
    'share-start'       : ['Start share',       'shareStart'],
    'share-success'     : ['Share success',     'shareSuccess'],
    'share-cancel'      : ['Cancel share',      'shareX'],
    'item-add-start'    : ['Add item start',    'addItemStart'],
    'item-add'          : ['Add item',          'addItem'],
    'item-add-cancel'   : ['Cancel add item',   'addItemX'],
    'list-item-add'     : ['Add item',          'addListItem'],
    'list-item-save'    : ['Save list item',    'saveListItem'],
    'list-save'         : ['Save list',         'saveList'],
    'list-save-cancel'  : ['Cancel list save',  'saveListX']
  },

  trackListAction: function(listId, action, description) {
    var arr = Tracker.actions[action];
    if ('undefined' != typeof(arr) && arr.length > 1) {
      var tag = arr[1];
      var path = Tracker.constants['LIST_PATH_PREFIX'] + "/" + listId + "/" + tag;
      Tracker.invokeTracker(path);
    }
  },

  trackAction: function(action, description) {
    var arr = Tracker.actions[action];
    if ('undefined' != typeof(arr) && arr.length > 1) {
      var tag = arr[1];
      var path = "/" + tag;
      Tracker.invokeTracker(path);
    }
  },

  trackEvent: function(path) {
    Tracker.invokeTracker(path);
  },
  
  invokeTracker : function(path) {
    if ('undefined' == typeof(pageTracker)) {
      dbug.log('GA not loaded. Tracking path: '+path);
    } else {
      dbug.log('Tracking path: '+path);
      pageTracker._trackPageview(path);
    }
  }
};

/**
 * Branding interface.
 */

var Branding = new Class({

  $family: { name: 'branding' },
  Implements: [Options],

  options: {
    key:              'default',
    key_default:      'default',
    key_disney:       'disney',
    subkey_lost_s4:   'lost_s4'
  },

  initialize : function(options) {
    this.setOptions(options);
    dbug.log('key: ' + this.options.key);
  },

  getKey : function() {
    return this.options.key;
  },

  getSubKey : function() {
    return this.options.subkey;
  },

  getFullKey : function() {
    return this.getKey() + (this.getSubKey() ? ('_' + this.getSubKey()) : '');
  },

  isDefault : function() {
    return this.getKey() == options.key_default;
  },

  getBuilder : function() {
      switch (this.getFullKey()) {
        case 'disney_lost_s4' :
          return new Builder.Lost();
        default:
           return new Builder();
      }
  },

  getSearcher : function() {
      switch (this.getFullKey()) {
        case 'disney_lost_s4' :
          return new Searcher.Lost();
        default:
           return new Searcher();
      }
  },

  getPublish : function() {
    return new Publish();
  }

});




/**
 * The list building interface.
 */
var Builder = new Class({

  $family: { name: 'builder' },
  Extends: Model,
  Implements: [Events, Chain, Options],

  options: {
    item_class: 'Item',
    disable_sortable_on_edit: true
  },

  /**
   * @constructor
   */
  initialize: function(options) {
    var that = this;

    this.setOptions(options);
    Item.setup();

    // initialize Model
    this.parent(document.body);
    this.addField('title', 'input.list-title', 'h1.list-title span');
    this.addField('description', '.list-module-story-content');
    this.addField('category_id', 'select[name*=category_id]');
    this.addField('branding_key', 'input[name*="branding_key"]');
    this.addField('branding_subkey', 'input[name*="branding_subkey"]');

    // element shortcuts
    this.list             = $('list');
    this.listForm         = $('list_form');
    this.listItems        = $('list_items');
    this.image            = $('list_image');
    this.adder            = $('list_adder');
    this.rightColMask     = $('right_column_mask');
    this.listOverlay      = $('list_items_overlay');
    this.activeOpinionTab = $('tab-active-opinion');

    // items
    var itemProto = eval(this.options.item_class);
    this.items = this.listItems.getChildren().map(function(element) { return new itemProto(element, that); });
    this.currentItem = this.items.find(function(item) { return item.isEditing(); });
    this.currentItem && this.currentItem.open();

    if (!this.isNewTopic()) {
      this.explorer = new Explorer(this);
    }

    // complex interactions
    this.initializeSaving();
    // this.initializeStory();
    this.initializeDiscussion();
    this.initializeItems();
    this.initializeCollabsables();

    // other behaviors
    if (this.listOverlay) {
      this.listOverlay.addEvent('click', function(event) {
        event.stop();
        $(event.target).destroy();
      });
    }

    this.adder.addEvent('click', function(event) {
      event.stop();
      that.closeItems().addItem();
    });
  },

  /**
   * Update total item numbers
   * 
   */
  updateListNumbers: function(){
      var numberOfWhat = 'item';
      if(this.items.length != 1){
          numberOfWhat += 's';
      }
      $('list_scroller_up').getElement('small').set('text', this.items.length+' '+numberOfWhat);
  },
  
  /**
   * Initializes list story interactions.
   * 
   * @return Builder
   */
  initializeStory: function() {
    var that = this;

    // element shortcuts
    this.storyLink        = $('list_topic_container').getElement('a[href$=list_story]');
    this.storyContainer   = $('list_story');
    this.storyEditor      = this.storyContainer.getElement('.editor');
    this.storyEditorField = this.storyEditor.getElement('textarea');
    this.storyPresenter   = this.storyContainer.getElement('.presenter');

    function editStory() {
      that.storyLink.hide();
      that.storyContainer.show();
      that.storyPresenter.hide();
      that.storyEditor.show().getElement('textarea').focus();
    }

    function closeStory() {
      var story = that.storyEditor.getElement('textarea').get('value');
      if (story) {
        that.storyLink.hide();
        that.storyEditor.hide();
        that.storyPresenter.set('text', story).show();
      } else {
        that.storyLink.show();
        that.storyPresenter.empty();
        that.storyContainer.hide();
      }
    }

    this.storyLink.addEvent('click', function(event) {
      event.stop();
      editStory();
    });
    this.storyPresenter.addEvent('click', editStory);
    this.storyEditorField.addEvents({
      blur: closeStory,
      keydown: function(event) {
        if ('esc' == event.key || ('enter' == event.key && !event.shift)) {
          event.stop();
          closeStory();
        }
      }
    });

    return this;
  },

  /**
   * Initializes the group discussion.
   * 
   * @return Builder
   */
  initializeDiscussion: function() {
    var that = this;

    this.discussionForm = $('add_comment_form');
    if (this.discussionForm) {
      this.discussionList = $('discussion_list');

      this.discussionForm.set('send', {
        onSuccess: function(responseText) {
          $('comment_content').set('value', '');
          var comment = new Element('ul', { html: responseText }).getFirst();
          that.discussionList.grab(comment);
          new Fx.Scroll(that.discussionList).toBottom().chain(function() {
            comment.highlight();
          });
        }
      }).addEvent('submit', function(event) {
        event.stop();
        $(event.target).send();
      });
    }

    return this;
  },

  /**
   * Initialize saving.
   * 
   * @return Builder
   */
  initializeSaving: function() {
    var that = this, saveButton = $('list_save_button'), publishButton = $('publish_widget');

    // setup new and modified flags
    this.setNew('post' == this.listForm.get('method'));
    this.initializeModifiedEvents();

    // override some default behaviors
    this.listForm.addEvent('submit', function(event) { event.stop(); });
    window.onbeforeunload = function() { if (that.requiresSave()) return 'Your list has not been saved.'; };

    // setup list save interactions
    publishButton.addEvent('click', function(event) {
      event.stop();
      if (that.requiresSave()) {
        that.clearChain();
        that.chain(function() {
          window.location.href = that.getPermalink('lists_publish');
        }).save();
      } else {
        window.location.href = that.getPermalink('lists_publish');
      }
    });

    saveButton.addEvent('click', function(event) {
      event.stop();
      that.clearChain();
      that.chain(function(json) {
        Modalbox.getInstance().show(that.getNextStepsElement());
      }).save();
    });

    return this;
  },

  /**
   * Observes form fields for changes.
   * 
   * @see Builder#initializeSaving
   * 
   * @param  {Element} container
   * @return Builder
   */
  initializeModifiedEvents: function(container) {
    var that = this;
    ($(container) || this.listForm).getElements('input, textarea, select').addEvent('change', function(event) {
      that.setModified(true);
    });
    return this;
  },

  /**
   * Initializes Item listeners.
   * 
   * @return Builder
   */
  initializeItems: function() {
    var that = this, scrollable = !!this.listItems.getParent('.scrollable');

    dbug.log(scrollable ? 'Initializing scrollable widget' : 'Initializing non-scrollable widget');

    this.listItems.getChildren().set('tween', { duration: 'short' });

    if (scrollable) {
      this.mooScroller = new MooScroller(this.listItems, 'fake_knob', {
        hideWhenNoOverflow: false,
        scrollLinks: { forward: 'list_scroller_down', back: 'list_scroller_up' }
      });
    }

    this.scroller = new Scroller(scrollable ? this.listItems : window, { area: 48, velocity: .25 });

    this.sortable = new Sortables(this.listItems, {
      constrain: true,
      clone: true,
      onStart: function(element, clone) { that.sortStart(element, clone); },
      onComplete: function(element) { that.sortComplete(element); }
    });
    this.resetSortable();

    this.addEvents({
      'item:contentChange': function(item) {
        // update the item image first
        item.updateImage();
        that.updateImage().setModified(true);
      },

      'item:destroy': function(item) {
        this.items.erase(item);
        that.updateImage().rankItems().setModified(true);
        that.rightColMask.hide();
        that.resetSortable();
      },

      'item:close': function(item) {
        that.currentItem = null;

        // delay to allow any close + open chains to complete
        (function() {
          that.resetSortable();
          that.currentItem || that.rightColMask.hide();
        }).delay(250);
      },

      'item:open': function(item) {
        that.currentItem = item;
        that.resetSortable();
        Tracker.trackListAction(2, 'list-item-open', 'open item');
      }
    });

    return this;
  },

  /**
   * Sort start logic.
   * 
   * @param {Element} element
   * @param {Element} clone
   */
  sortStart: function(element, clone) {
    var items = this.listItems.getChildren();

    if (element) {
      element.set('opacity', 0).store('dragged', true);
      items.erase(element);
    }

    if (clone) {
      items.erase(clone);
      clone.setStyle('zIndex', 1000);
    }

    items.tween('opacity', 0.5);
    this.scroller.start();
  },

  /**
   * Sort completion logic.
   * 
   * @param {Element} element
   */
  sortComplete: function(element) {
    var items = this.listItems.getChildren();

    if (element && element.retrieve('dragged')) {
      element.set('opacity', 1);
      items.erase(element);
      this.updateImage().rankItems().setModified(true);

      // clear open/close cancelling flag
      (function() { element.eliminate('dragged'); }).delay(500);
    }

    items.tween('opacity', 1);
    this.scroller.stop();
  },

  /**
   * Resets sortable list items.
   * 
   * @return Builder
   */
  resetSortable: function() {
    this.listItems.removeClass('sortable');
    this.sortable.detach();
    if (
      this.items.length > 1
      &&
      (!this.options.disable_sortable_on_edit || !this.items.some(function(item) { return item.isEditing(); }))
    ) {
      this.listItems.addClass('sortable');
      this.sortable.attach();
    }
    this.updateListNumbers();

    return this;
  },

  /**
   * Initializes collapsables.
   * 
   * @return Builder
   */
  initializeCollabsables: function() {
    $$('.collapsable').each(function(element) {
      if (element.get('collapsable:instance')) return;
      var clicker = element.getElement('.show'), section = element.getElement('.drawer');
      if (clicker && section) {
        var collapsable = new Collapsable(clicker, section, {
          duration: 'short',
          onShow: function() { clicker.getElement('small').set('text', 'hide').toggleClass('arrow-down'); },
          onHide: function() { clicker.getElement('small').set('text', 'show').toggleClass('arrow-down'); }
        });
        element.store('collapsable:instance', collapsable);
      }
    });

    return this;
  },

  /**
   * Saves the current list.
   * 
   * Calls the function chain upon successful save, or if no save is
   * necessary. If a save was performed, the JSON list response is passed to
   * the first chained function.
   * 
   * @return Builder
   */
  save: function() {
    var that = this;

    if (!this.validate()) {
      return this;
    }

    // todo: should this be part of validate?
    if (!this.isModified()) {

      if (this.isNewTopic()) {
        alert('Please complete your list first.');
        Modalbox.getInstance().hide();
      } else {
        dbug.log('No save necessary; executing chain.');
        this.callChain();
      }

      return this;
    }

    dbug.log('Performing save request.');
    dbug.log(JSON.encode({ list: this.getValues() }));

    new Request.JSON({
      url: this.listForm.get('action'),
      method: this.isNew() ? 'post' : 'put',
      emulation: false,
      urlEncoded: false,
      data: JSON.encode({ list: this.getValues() }),
      bouncerOptions: { leaving: true },
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'X-Request': 'JSON',
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8'
      },
      onRequest: function() {
        dbug.log('Locking screen (saving...)');
        Modalbox.getInstance().show(new Element('div', { 'class': 'save-share', html: 'saving...' }), { locked: true });
        that.fireEvent('saveRequest');
      },
      onSuccess: function(json) {
        dbug.log('Processing successful save.');
        that.listForm.set('action', json.uuid).set('method', 'put');
        that.setNew(false).setModified(false).fireEvent('saveSuccess').callChain(json);
      },
      onFailure: function(xhr) {
        dbug.log('Processing failed save. '+xhr.responseText);

        var errors = '';
        $H(JSON.decode(xhr.responseText)).each(function(value, key) {
          errors += key+': '+value;
        });

        alert("Please correct the following errors:\n\n"+errors);
        Modalbox.getInstance().hide();

        that.fireEvent('saveFailure');
      },
      onComplete: function() {
        dbug.log('Save complete');
        that.fireEvent('saveComplete');
      }
    }).send();

    return this;
  },

  /**
   * @see Model
   */
  getValues: function() {
    var values = $extend(this.parent(), {
      // token: this.element.getElement('input[name="list[token]"]').get('value'),
      item: this.listItems.getChildren().map(function(element) { return element.retrieve('model').getValues(); })
    });

    if (!this.isNewTopic()) {
      // remove title & category id
      values = $H(values).filter(function(value, key) { return -1 == ['title', 'category_id'].indexOf(key); }).getClean();
    }

    return values;
  },

  /**
   * Adds a new item to the list.
   * 
   * Available options:
   * 
   *  * {Item}   after Add new item after this one
   *  * {String} title Prepopulate item with a title
   * 
   * @param  {Object} options
   * @return Item
   */
  addItem: function(options) {
    options = options || { };
    var item = Item.createInstance(this).inject(options.after).open();
    options.title && item.setTitle(options.title);
    this.initializeModifiedEvents(item.element);
    this.items.push(item);
    this.rankItems();
    return item;
  },

  /**
   * Updates the list image with the highest item image.
   * 
   * @return Builder
   */
  updateImage: function() {
    // fixme: if the internal stack of items was ordered we could loop through that
    var sourceImage = this.listItems.getElements('img.item-image').find(function(element) {
      return !element.get('src').contains(Builder.defaultImage);
    });
    this.image.set('src', sourceImage ? sourceImage.get('src') : Builder.defaultImage);
    return this;
  },

  /**
   * Close all items.
   * 
   * @param  {Item} except An item that should not be closed
   * @return Builder
   */
  closeItems: function(except) {
    for (var i = 0, l = this.items.length; i < l; i++) {
      except != this.items[i] && this.items[i].isEditing() && this.items[i].close();
    }
    return this;
  },

  /**
   * Updates ranking numbers on all items.
   * 
   * @return Builder
   */
  rankItems: function() {
    for (var i = 0, l = this.items.length; i < l; i++) {
      this.items[i].updateRank();
    }
    return this;
  },

  /**
   * Returns the next steps window.
   * 
   * @param  {Object} options
   * @return Element
   */
  getNextStepsElement: function() {
    var that = this;

    if (!this.nextStepsElement) {

      if (this.isNewTopic()) {
        alert('An unexpected error occurred.');
        return;
      }

      // assume the list has been saved and the form action is the uuid
      $('next-step-view').set('href', that.getPermalink()).removeEvents();
      $('next-step-publish').set('href', that.getPermalink('lists_publish')).removeEvents();
      $('next-step-share').addEvent('click', function(event) {
        event.stop();
        if ($(document.body).hasClass('explorer')) {
          Main.share.showPopup();
        } else {
          that.closeItems();
          Modalbox.getInstance().hide();
        }
      });

      this.nextStepsElement = $('next-steps-dialog').show();
    }

    return this.nextStepsElement;
  },

  /**
   * Returns the current list's permalink.
   * 
   * @param  {String} command
   * @return String
   */
  getPermalink: function(command) {
    var link = this.isNewTopic() ? null : this.listForm.get('action');
    if (link && command) link = link.replace('/lists/', '/'+command+'/');
    return link;
  },

  /**
   * Returns true if the current list needs to be saved before doing something with it.
   * 
   * @return Boolean
   */
  requiresSave: function() {
    // a new but unmodified list that belongs to an existing topic does not have to be saved
    if (this.isNew() && !this.isModified() && !this.isNewTopic()) {
      return false;
    }
    return this.isNew() || this.isModified();
  },

  /**
   * Sets the modified flag.
   * 
   * @param  {Boolean} modified
   * @return Builder
   */
  setModified: function(value) {
    dbug.log('Setting modified flag to '+value);
    this.activeOpinionTab && this.activeOpinionTab[value ? 'addClass' : 'removeClass']('unsaved-changes');
    this.$modified = value;
    return this;
  },

  /**
   * Returns true if the current list has been modified.
   * 
   * @return Boolean
   */
  isModified: function() {
    return this.$modified ? true : false;
  },

  /**
   * Sets the new flag.
   * 
   * @param  {Boolean} value
   * @return Builder
   */
  setNew: function(value) {
    dbug.log('Setting new flag to '+value);
    this.$new = value;
    return this;
  },

  /**
   * Returns true if a list item is being edited.
   *
   * @return Boolean
   */
   isEditing: function() {
     var items = this.items;
     for (var i = 0; i < items.length; i++) {
       if (items[i].isEditing())
          return true;
     }     
     return false;
   },

  /**
   * Returns true if the current list has never been saved to the database.
   * 
   * @return Boolean
   */
  isNew: function() {
    return this.$new ? true : false;
  },

  /**
   * Returns true if saving this list will create a new topic.
   * 
   * @return Boolean
   */
  isNewTopic: function() {
    return this.listForm.get('action').match(/lists$/);
  },

  /**
   * Returns true if the supplied item value is valid.
   * 
   * @return Boolean
   */
  itemValueIsValid: function(item) {
    return $defined(item) && $defined(item.title);
  },

  /**
   * Validates the current list form values.
   * 
   * @return Boolean
   */
  validate: function() {
    var values = this.getValues();

    if (this.isNewTopic() && (!values || !$defined(values.title))) {
      alert("Please give your list a title");
      return false;
    }

    if (!values || values.item.length < 1 || (values.item.length == 1 && !this.itemValueIsValid(values.item[0]))) {
      alert("Please add at least one item to the list");
      return false;
    }

    return true;
  }
});

Builder.defaultImage = Dynamic.defaultBuilderImage;

/**
 * Represents the list builder for LOST lists.
 */
Builder.Lost = new Class({

  Extends: Builder,

  /**
   * @constructor
   */
  initialize: function(options) {
    options = $extend({
      item_class: 'Item.Lost',
      disable_sortable_on_edit: false
    }, options || { });
    this.parent(options);
  },

  /**
   * Opens the LOST item chooser.
   * 
   * Available options:
   * 
   *  * {Item} after Add new item after this one
   * 
   * @param  {Object} options
   * @return Builder.Lost
   */
  addItem: function(options) {
    var that = this;
    options = options || { };
    Main.searcher.open({
      onAdd: function(data) {
        var item = Item.createInstance(that, Item.Lost);
        item.hydrate(data).inject(options.after);
        that.initializeModifiedEvents(item.element);
        that.items.push(item);
        that.rankItems();
        that.resetSortable();
        // because it works...
        Main.builder.fireEvent('item:contentChange', item);
        Main.setupJavascriptLinks(item.element);
      }
    });
    return this;
  }

});

/**
 * Abstraction of a list item.
 * 
 * Events:
 *  * contentChange
 *  * titleChange
 *  * open
 *  * close
 *  * destroy
 */
var Item = new Class({

  $family: { name: 'item' },
  Extends: Model,
  Implements: Events.Bubbler,

  /**
   * @constructor
   */
  initialize: function(element, builder) {
    var that = this;

    this.parent(element);
    this.bubblesTo(builder, 'item');
    this.builder = builder;

    // element shortcuts
    this.header           = this.element.getElement('.item-header');
    this.image            = this.element.getElement('img');
    this.titleHeading     = this.element.getElement('h2');
    this.titleField       = this.element.getElement('input.title');
    this.descriptionField = this.element.getElement('input.description');
    this.rank             = this.element.getElement('.rank');
    this.dropZone         = this.element.getElement('.item-attachments');
    this.dropInstructions = this.dropZone.getElement('.drag-instructions');
    this.deleter          = this.element.getElements('.delete, .delete-item');
    this.dragHandle       = this.element.getElement('.drag');
    this.closers          = this.element.getElements('.done');
    this.searcherToggler  = this.element.getElements('.add-content');

    this.addField('id', 'input[name$="[id]"]');
    this.addField('use_id', 'input[name$="[use_id]"]');
    this.addField('title', [this.titleField, this.titleHeading]);
    this.addField('description', this.descriptionField);

    this.header.addEvent('click', function(event) {
      event.stop();
      // cancel click if Item was just dragged
      if (that.element.retrieve('dragged')) {
        that.element.eliminate('dragged');
        return;
      }

      // not opennable for now
      if (that.isReadOnly()) return;

      if (that.isEditing()) {
        that.isReadOnly() && that.close();
      } else {
        that.open();
      }
    });

    if (this.isReadOnly()) {
      this.dropZone.getChildren().erase(this.dropInstructions).each(function(element) {
        new Content(element);
      });
      return;
    }

    this.itemContents = [];
    this.dropZone.getChildren().erase(this.dropInstructions).map(function(element) {
      return new ItemContent(that, element);
    });

    this.sortable = new Sortables(this.dropZone, {
      constrain: true,
      clone: true,
      onStart: function(element, clone) {
        element.set('opacity', 0).getSiblings().erase(clone).set('morph', { duration: 'short' }).morph({ opacity: 0.5 });
        clone.setStyle('zIndex', 1000);
      },
      onSort: function(element, clone) {
        that.fireEvent('contentChange');
      },
      onComplete: function(element) {
        element.set('opacity', 1).getSiblings().morph({ opacity: 1 });
      }
    });
    if (!this.itemContents.length) this.sortable.detach();

    var contentChange = function() {
      that.dropInstructions && that.dropInstructions.toggleTo(that.itemContents.length < 3);
      that.sortable[that.itemContents.length > 1 ? 'attach' : 'detach']();
    };
    contentChange();

    // behaviors
    this.addEvents({
      close: function() { Tips.instance.hide(); },
      destroy: function() { Tips.instance.hide(); },
      contentChange: contentChange
    });

    this.titleField.addEvent('focus', function() {
      // a pseudo-solution (assumes focus means edit)
      that.titleFieldHasReceivedFocus = true;
    });

    this.closers.addEvent('click', function(event) {
      event.stop();
      that.isEditing() && that.close();
    });

    this.deleter.addEvent('click', function(event) {
      event.stop();
      that.destroy();
    });

    this.searcherToggler.addEvent('click', function(event) {
      event.stop();
      Main.searcher.toggle(that);
      that.resetSearcherToggler();
    });

    Watermark.setup(this.element);

    this.element.getElement('.delete').addTip({ offsetX: -7, offsetY: -42 });
    this.titleHeading.addTip({ offsetX: -7, offsetY: -42 });
    this.dragHandle.addTip({ offsetX: -7, offsetY: -42 });
  },

  /**
   * Resets the current item's search toggler.
   * 
   * @return Item
   */
  resetSearcherToggler: function() {
    if (Main.searcher && Main.searcher.isOpen()) {
      this.searcherToggler.removeClass('add-content').addClass('add-content-open');
    } else {
      this.searcherToggler.removeClass('add-content-open').addClass('add-content');
    }
    return this;
  },

  /**
   * Updates the Item image to reflect the top content piece.
   * 
   * @return Item
   */
  updateImage: function() {
    var sourceImage = this.dropZone.getElement('img');
    this.image.set('src', sourceImage ? sourceImage.get('src') : this.builder.defaultImage);
    return this;
  },

  /**
   * @see Model
   */
  getValues: function() {
    return $extend(this.parent(), {
      content: this.dropZone.getChildren().erase(this.dropInstructions).map(function(element) {
        return element.retrieve('model').getValues();
      })
    });
  },

  /**
   * Sets the displayed rank of the current item.
   * 
   * @param  {Number} rank
   * @return Item
   */
  setRank: function(rank) {
    this.rank.set('text', rank);
    return this;
  },

  /**
   * Injects the current Item into the Builder.
   * 
   * @param  {Item} afterItem Inject after this item
   * @return Item
   */
  inject: function(afterItem) {
    // inject after supplied Item or at the bottom of the list
    afterItem ?
      this.element.inject(afterItem.element, 'after') :
      this.element.inject(this.builder.listItems);
    return this;
  },

  /**
   * Hydrates the current item with data.
   * 
   * @param  {Object} data
   * @return Item
   */
  hydrate: function(data) {
    this.element.getElement('input[name$="[id]"]').set('value', data.id);
    this.element.getElement('input[name$="[use_id]"]').set('value', data.use_id);
    this.titleField.set('value', data.title);
    this.titleHeading.set('text', data.title);
    this.descriptionField.set('value', data.description);

    if (data.image) this.image.set('src', data.image);

    for (var i = 0, l = (data.contents || []).length; i < l; i++) {
      Content.getInstance(data.contents[i]).injectIntoItem(this);
    }

    return this;
  },

  /**
   * Places window focus in the title field.
   * 
   * @return Item
   */
  focus: function() {
    this.titleField.focus();
    return this;
  },

  /**
   * Opens the current item for editing.
   * 
   * @return Item
   */
  open: function() {
    var that = this;

    this.builder.closeItems(this);

    this.observer = this.observer ? this.observer.resume() : new Observer(this.titleField, function(value) {
      that.fireEvent('titleChange', value);
    }, { delay: 500 });

    // this.titleField.set('value', this.titleHeading.get('text'));
    this.element.addClass('edit');

    var parent = this.element.getParent(), fx = new Fx.Scroll(parent);
    fx.chain(function() { that.fireEvent('open', that); });
    fx.start(0, this.element.getPosition(parent).y);

    this.resetSearcherToggler();

    return this;
  },

  /**
   * Closes the current item.
   * 
   * @return Item
   */
  close: function() {
    if (!this.isReadOnly()) {
      if (!this.isValid()) return this.destroy();
      this.titleHeading.set('text', this.getTitle());
    }

    this.element.removeClass('edit');
    this.observer && this.observer.pause();
    this.fireEvent('close');

    return this;
  },

  /**
   * Removes the current Item from the DOM and destroys its references.
   * 
   * return Item
   */
  destroy: function() {
    this.element.dispose();
    this.fireEvent('destroy');

    // finally destroy the item after the event has fired
    this.element.destroy();

    return this;
  },

  /**
   * Returns true if the current Item's title was set directly by the user.
   * 
   * @return Boolean
   */
  hasRealTitle: function() {
    return this.isNew() ? $chk(this.titleFieldHasReceivedFocus) : true;
  },

  /**
   * Returns the current Item's title.
   * 
   * @return String
   */
  getTitle: function() {
    if (this.isReadOnly()) {
      return this.getValue('title');
    }
    var itemContent = this.getPrimaryItemContent();
    return Watermark.isEmpty(this.titleField) ? (itemContent ? itemContent.content.getTitle() : '') : this.titleField.get('value');
  },

  /**
   * Sets the current Item's title.
   * 
   * @return Item
   */
  setTitle: function(title) {
    this.titleField.set('value', title).fireEvent('blur', { target: this.titleField });
    return this;
  },

  /**
   * Returns the primary ItemContent object.
   * 
   * @return ItemContent|null
   */
  getPrimaryItemContent: function() {
    var elements = this.dropZone.getChildren().erase(this.dropInstructions);
    return elements[0] ? elements[0].retrieve('model') : null;
  },

  /**
   * Detects rank and updates presentation accordingly.
   * 
   * @return Item
   */
  updateRank: function() {
    this.rank.set('text', this.element.getParent().getChildren().indexOf(this.element)+1);
    return this;
  },

  /**
   * Returns true if the current item is new.
   * 
   * @return Boolean
   */
  isNew: function() {
    return !$chk(this.getValue('id'));
  },

  /**
   * Returns true if this item is opened.
   * 
   * @return Boolean
   */
  isEditing: function() {
    return this.element.hasClass('edit');
  },

  /**
   * Returns true if the item is read-only.
   * 
   * @return Boolean
   */
  isReadOnly: function() {
    return this.element.hasClass('read-only');
  },

  /**
   * Returns true if the item validates for save.
   * 
   * @return Boolean
   */
  isValid: function() {
    return $chk(this.getTitle() || this.itemContents[0]);
  }

});

/* ------------------------------------------------------------------------ */

Item.extend({

  /**
   * Sets up item template.
   */
  setup: function() {
    Item.template = new Element('ul', { html: Item.templateHtml }).getFirst();
    Item.readOnlyTemplate = new Element('ul', { html: Item.readOnlyTemplateHtml }).getFirst();
  },

  /**
   * Spawns a new Item object.
   * 
   * @param  {Builder} builder
   * @return Item
   */
  createInstance: function(builder, options) {
    options = $extend({
      itemClass: Item,
      readOnly: false
    }, options || { });

    var element = (options.readOnly ? Item.readOnlyTemplate : Item.template).clone();
    return new options.itemClass(element, builder);
  }

});

Item.templateHtml = $(document.body).className.match(/\bdisney_lost_s4\b/) ? Dynamic.itemTemplateLost : Dynamic.itemTemplate;
Item.readOnlyTemplateHtml = Dynamic.readOnlyItemTemplate;

/**
 * Represents an item in a LOST list.
 */
Item.Lost = new Class({

  Extends: Item,
  Implements: Options,

  options: {
    opener_selector: 'div.item',
    closer_selector: 'img.done',
    remover_selector: 'img.delete',
    previewer_selector: 'a.content',
    rank_selector: 'span.rank',
    data_selector: 'input[name=data]',
    title_selector: '.item-title',
    description_selector: 'div.presentation p',
    image_selector: 'img.item-image, a.content > img',
    dont_bubble: false
  },

  initialize: function(element, builder, dontBubble, options) {
    this.setOptions($extend({ dont_bubble: dontBubble }, options || { }));
    this.setElement(element);
    this.setBuilder(builder);

    this.bound = {
      open: this.open.bind(this),
      close: this.close.bindWithEvent(this),
      remove: this.remove.bindWithEvent(this),
      preview: this.preview.bind(this)
    };

    this.initializeOpenerElements();
    this.initializeCloserElements();
    this.initializeRemoverElements();
    this.initializePreviewerElements();
    this.initializeRankElements();
    this.initializeDataElement();
    this.initializeTitleElements();
    this.initializeDescriptionElements();
    this.initializeImageElements();
  },

  setElement: function(element, options) {
    this.element = $(element);
    this.element.store('model', this);
    return this;
  },

  setBuilder: function(builder, options) {
    this.builder = builder;
    this.options.dont_bubble || this.bubblesTo(this.builder, 'item');
    return this;
  },

  initializeOpenerElements: function() {
    this.openers = this.element.getElements(this.options.opener_selector);
    this.openers && this.openers.addEvent('click', this.bound.open);
    return this;
  },

  initializeCloserElements: function() {
    this.closers = this.element.getElements(this.options.closer_selector);
    this.closers && this.closers.addEvent('click', this.bound.close);
    return this;
  },

  initializeRemoverElements: function() {
    this.removers = this.element.getElements(this.options.remover_selector);
    this.removers && this.removers.addEvent('click', this.bound.remove);
    return this;
  },

  initializePreviewerElements: function() {
    this.previewers = this.element.getElements(this.options.previewer_selector);
    this.previewers && this.previewers.addEvent('click', this.bound.preview);
    return this;
  },

  initializeRankElements: function() {
    this.rank = this.element.getElements(this.options.rank_selector);
    return this;
  },

  initializeDataElement: function() {
    var element = this.element.getElement(this.options.data_selector);
    if (element) {
      this.data = JSON.decode(this.element.getElement(this.options.data_selector).get('value'));
    }
    return this;
  },

  initializeTitleElements: function() {
    this.titles = this.element.getElements(this.options.title_selector);
    return this;
  },

  initializeDescriptionElements: function() {
    this.descriptions = this.element.getElements(this.options.description_selector);
    return this;
  },

  initializeImageElements: function() {
    this.images = this.element.getElements(this.options.image_selector);
    return this;
  },

  hydrate: function(data) {
    this.data = data;
    this.titles.set('text', data.title);
    this.descriptions.set('text', data.description);
    data.image && this.images.set('src', data.image);
    return this;
  },

  getValues: function() {
    var data = $H(this.data), ok = ['id', 'use_id', 'title', 'description'];
    data.getKeys().each(function(key) { ok.indexOf(key) > -1 || data.erase(key); });
    return data.getClean();
  },

  open: function() {
    if (!this.isEditing()) {
      this.builder.closeItems(this);
      this.element.addClass('edit');
      this.fireEvent('open', this);
    }
  },

  close: function(event) {
    event && event.stop();
    this.element.removeClass('edit');
    this.fireEvent('close');
  },

  remove: function(event) {
    event.stop();
    this.element.destroy();
    this.fireEvent('destroy');
  },

  preview: function() {
    var div = new Element('div', { 'class': 'module', id: 'video_preview_modal'}),
      title = new Element('h2', { text: this.data.title }).inject(div),
      swf = new Swiff(this.getSwfUrl(), { width: 408, height: 327 }).inject(div);

    Modalbox.getInstance().show(div, { pleaseDestroy: true });
    this.fireEvent('preview', this);

    return this;
  },

  getSwfUrl: function() {
    return Context.swf+'/mediaplayer.swf?autostart=true&width=408&height=327&file='+encodeURIComponent(this.data.url);
  },

  updateImage: $empty

});

/**
 * Represents an item in the holding pen.
 */
Item.HoldingPen = new Class({

  Extends: Item,
  Implements: Options,

  options: {
    
  },

  initialize: function(element, builder, options) {
    this.element = $(element);
    this.builder = builder;
    this.setOptions(options);

    this.titleHeading = this.element.getElement('h2');
    this.image = this.element.getElement('img');
    this.adder = this.element.getElement('.add-to-list');

    this.bound = {
      open: this.open.bind(this),
      close: this.close.bind(this),
      addToList: this.addToList.bind(this)
    };

    this.element.addEvent('click', this.bound.open);
    this.adder.addEvent('click', this.bound.addToList);
  },

  open: function() {
    this.element.getSiblings().each(function(element) { element.retrieve('pen:item').close(); });
    this.element.addClass('edit').removeEvents('click').addEvent('click', this.bound.close);
  },

  close: function() {
    this.element.removeClass('edit').removeEvents('click').addEvent('click', this.bound.open);
  },

  addToList: function(clone) {
    if ('event' == $type(clone)) clone.stop();

    var item = Item.createInstance(this.builder).hydrate(this.element.retrieve('item:data'));
    if ('element' == $type(clone)) {
      item.element.replaces(clone);
    } else {
      item.inject();
    }

    this.builder.initializeModifiedEvents(item.element);
    this.builder.items.push(item);
    this.builder.rankItems();

    this.builder.explorer.resetHoldingPen();
    this.builder.resetSortable();
  },

  hydrate: function(data) {
    this.element.store('item:data', data);
    this.titleHeading.set('text', data.title);

    if (data.image) this.image.set('src', data.image);

    for (var i = 0, l = (data.contents || []).length; i < l; i++) {
      Content.getInstance(data.contents[i]).element.inject(this.element.getElement('.item-attachments'));
    }

    return this;
  },

  isReadOnly: function() {
    return true;
  }

});

/**
 * Abstraction for the content searcher.
 * 
 * Assumes only one searcher exists per page.
 */
var Searcher = new Class({

  $family: { name: 'searcher' },

  /**
   * @constructor
   */
  initialize: function() {
    var that = this;

    this.portal  = $('content_browser_container');
    this.element = $('content_browser');

    this.typeTabsList  = $('content_browser_type_tabs');
    this.typeTabs      = this.typeTabsList.getElements('a');
    this.searchFs      = $('content_browser_search_fs');
    this.refineField   = this.searchFs.getElement('input.refine');
    this.button        = this.searchFs.getElement('button');
    this.results       = $('results');
    this.pager         = $('content_browser_pager');
    this.pagerPrevious = $('content_browser_pager_left');
    this.pagerNext     = $('content_browser_pager_right');
    this.noResults     = $('content_browser_no_results');
    this.pagerLimit    = 3;

    this.searchForm      = $('content_browser_search_form');
    this.searchFormField = this.searchForm.getElement('input[type=text]');
    this.searchedItems   = [];

    this.element.getElements('.done').removeEvents('click').addEvent('click', function(event) {
      event.stop();
      that.isOpen() && that.hide();
    });

    this.contents = [];

    // pseudo-submit behavior
    this.refineField.addEvent('keypress', function(event) {
      if ('enter' == event.key) {
        event.stop();
        that.search(that.refineField.get('value'));
      }
    });
    this.button.addEvent('click', function(event) {
      event.stop();
      that.search(that.refineField.get('value'));
    });

    new Observer(this.refineField, function(value, element) {
      // only set the item title if it has not been directly set by the user already
      var item = Main.builder.currentItem;
      item && !item.hasRealTitle() && item.setTitle(value);
    }, { delay: 0 });

    this.pagerPrevious.addEvent('click', function(event) {
      event && event.stop();
      if (that.pagerPrevious.hasClass('active')) {
        that.injectContents(that.cursor - 1, that.pagerLimit);
      }
    });

    this.pagerNext.addEvent('click', function(event) {
      event && event.stop();
      if (that.pagerNext.hasClass('active')) {
        that.injectContents(that.cursor + 1, that.pagerLimit);
      }
    });

    this.typeTabsList.getElements('a').addTip({ offsetY: -42, text: true });

    // content types
    this.tabSwapper = new TabSwapper({
      onActive: function(i, section, tab) {
        // delay so TabSwapper can update class names
        that.search.delay(100, that, that.lastQuery);
        that.searchForm[tab.hasClass('yelp') ? 'addClass' : 'removeClass']('yelp-search');
      },
      rearrangeDOM: false,
      selectedClass: 'current',
      tabs: this.typeTabs,
      sections: (function() {
        // fake sections
        var sections = new Array();
        for (i = 0, l = that.typeTabsList.getChildren().length; i < l; i++)
          sections.push(new Element('div').hide());
        return sections;
      })()
    });

    if (Main.builder.currentItem && this.isOpen()) {
      // fix show/hide content button
      Main.builder.currentItem.element.getElement('.add-content').removeClass('add-content').addClass('add-content-open');
    }

    Main.builder.addEvents({
      'item:contentChange': function(item) {
        that.isOpen() && that.resetContents(item);
      },
      'item:open': function(item) {
        if (item.isNew() && !item.isValid()) that.show(item);
        that.isOpen() && that.search.delay(100, that, item.getTitle());
      },
      'item:close': function() {
        that.isOpen() && that.hide();
      },
      'item:destroy': function() {
        that.isOpen() && that.hide();
      },
      'item:titleChange': function(item, value) {
        that.isOpen() && that.search.delay(100, that, value);
      }
    });
  },

  /**
   * Shows the searcher.
   * 
   * @return Searcher
   */
  show: function(item) {
    // considering padding-top: 24px style on this.element
    this.portal.setStyle('top', item.element.getTop() - this.portal.getParent().getTop() - 24).show();
    this.search(item.getTitle());
    Main.builder.rightColMask.show();
    return this;
  },

  /**
   * Returns true if the searcher is open.
   * 
   * @return Boolean
   */
  isOpen: function() {
    return this.portal && this.portal.isVisible();
  },

  /**
   * Toggles visibility of the searcher.
   * 
   * @param  {Item} item
   * 
   * @return Searcher
   */
  toggle: function(item) {
    this.isOpen() ? this.hide() : this.show(item);
    return this;
  },

  /**
   * Hides the searcher.
   * 
   * @return Searcher
   */
  hide: function() {
    this.portal.hide();
    this.waiter && this.waiter.stop();
    Main.builder.rightColMask.hide();
    return this;
  },

  /**
   * Executes a search against the currently selected content type.
   * 
   * @param  {String} query
   * @return Searcher
   */
  search: function(query) {
    var that = this;

    // keep the refine field up to date or watermarked
    this.refineField.set('value', query).fireEvent('blur', { target: this.refineField });

    this.emptyResults();

    if (query && query.trim()) {
      // set options only if they're not there already
      this.searchForm.retrieve('send:options') || this.searchForm.set('send', {
        onSuccess: this.process.bind(this),
        onRequest: function() {
          that.noResults.hide();
          (that.waiter = that.waiter || new Waiter(that.results, {
            baseHref: Context.images,
            img: { src: '/ls/background/content-search-progress.gif', styles: { width: 164, height: 94 } }
          })).start();
        },
        onComplete: function() {
          that.waiter.stop();
        }
      });

      this.lastQuery = query;
      this.searchForm.get('sendJson').setOptions({
        data: $extend(this.searchForm.serialize(), {
          limit: 50,
          query: query,
          content_type: this.getSelectedContentType(),
          current_item: Main.builder.currentItem ? Main.builder.currentItem.getValue('id') : ''
        })
      }).get();
    }

    return this;
  },

  /**
   * Processes search results.
   * 
   * @param {Object} json
   */
  process: function(json) {
    this.contents = [];
    this.total = json ? json.length : 0;

    if (this.total > 0) {
      if ($defined(json[0].contents)) {
        // items
        for (var i = 0, l = json.length; i < l; i++) {
          this.contents.push(new Item.Addable(json[i], this));
        }
        this.injectContents(0, 2);
      } else {
        // contents
        for (var i = 0, l = json.length; i < l; i++) {
          this.contents.push(Content.getInstance(json[i]));
        }
        this.injectContents(0, 3);
      }
    } else if ('items' == this.getSelectedContentType() && !this.searchedItems.contains(Main.builder.currentItem)) {
      // move on to youtube
      this.tabSwapper.show(1);
    } else {
      // no results
      this.noResults.show();
    }

    this.searchedItems.push(Main.builder.currentItem);
  },

  /**
   * Empties the contents of the results container and hides the pager.
   * 
   * @return  Searcher
   */
  emptyResults: function() {
    // dispose -- don't destroy or the Content instance will get screwed up
    this.results.getChildren().dispose();
    this.pager.hide();
    return this;
  },

  /**
   * Injects three Content elements into the results container.
   * 
   * @param   {Number} offset Defaults to 0
   * @return  Searcher
   */
  injectContents: function(offset, limit) {
    this.emptyResults().setCursor(offset || 0);

    this.pagerLimit = limit || 3;

    for (var i = this.cursor, l = i + this.pagerLimit; i < l; i++) {
      this.contents[i] && this.contents[i].injectIntoSearcher(this);
    }

    this.resetContents();

    // reset pager
    var start = this.cursor - this.pagerLimit - 1;
    if (start > (this.total - 1) - 11)
      start = (this.total - 1) - 11;
    if (start < 0)
      start = 0;

    this.pager.show();

    return this;
  },

  /**
   * Sets the cursor, enforces allowed range.
   * 
   * @param   {Number} cursor
   * @return  Searcher
   */
  setCursor: function(cursor) {
    var nextActive = true, previousActive = true;
    if (cursor <= 0) {
      // cannot be negative
      this.cursor = 0;
      previousActive = false;
    } else if (cursor >= this.total - this.pagerLimit - 1) {
      // cannot exceed 3rd from last (0-indexed)
      this.cursor = this.total - this.pagerLimit - 1;
      nextActive = false;
    } else {
      this.cursor = cursor;
    }

    // reset pager links
    this.pagerNext[nextActive ? 'addClass' : 'removeClass']('active');
    this.pagerPrevious[previousActive ? 'addClass' : 'removeClass']('active');

    return this;
  },

  /**
   * Resets all Content elements in the current result set.
   * 
   * @param   {Item} item The Item to reset to
   * @return  Searcher
   */
  resetContents: function(item) {
    for (var i = this.cursor, l = i + this.pagerLimit; i < l; i++) {
      this.contents[i] && this.contents[i].reset(item);
    }
    return this;
  },

  /**
   * Returns the currently selected content type.
   * 
   * @return String
   */
  getSelectedContentType: function() {
    return this.typeTabsList.getElement('.current').get('fragment');
  },

  /**
   * Returns those content types that aren't selected.
   * 
   * @return Array
   */
  getBackgroundContentTypes: function() {
    return this.typeTabsList.getElements('a').map(function(element) { return element.get('fragment'); }).erase(this.getSelectedContentType());
  }
});

/**
 * Represents the LOST item chooser.
 */
Searcher.Lost = new Class({

  Extends: Searcher,
  Implements: [Options, Events],

  options: {
    tab_links_selector: '#disney_lost_s4_item_chooser_tabs a',
    current_tab_class: 'current',
    tab_sections_selector: '#disney_lost_s4_item_chooser_sections > ul',
    tab_options: { },
    items_selector: 'ul.items > li',
    preview_container_selector: 'div.preview-container'
  },

  /**
   * @constructor
   */
  initialize: function(element, options) {
    var that = this;

    this.bound = {
      modalHide: this.modalHide.bind(this)
    };

    this.setOptions(options);
    this.setElement(element || 'disney_lost_s4_item_chooser');

    this.initializeItems();
    this.initializeTabSwapper();
    this.initializePreview();
  },

  setElement: function(element) {
    this.element = $(element);
    return this;
  },

  initializeItems: function(options) {
    var that = this;
    options = $extend(this.options, options || { });
    this.items = this.element.getElements(options.items_selector).map(function(element) {
      return new Searcher.Lost.Item(element, that, options);
    });
    return this;
  },

  initializeTabSwapper: function(options) {
    options = $extend(this.options, options || { });
    this.tabSwapper = new TabSwapper($extend({
      selectedClass: options.current_tab_class,
      mouseoverClass: '',
      rearrangeDOM: false,
      tabs: this.element.getElements(options.tab_links_selector),
      sections: this.element.getElements(options.tab_sections_selector),
      onActive: function(i, section, tab) {
        section.setStyle('overflow', 'auto');
      }
    }), options.tab_options || { });
    return this;
  },

  initializePreview: function() {
    this.previewContainer = this.element.getElement(this.options.preview_container_selector);
    this.initialPreviewHtml = this.previewContainer.get('html');
    return this;
  },

  open: function(options) {
    var that = this;
    this.items.each(function(item) {
      if (item.existsIn(Main.builder)) {
        item.disable();
      } else {
        item.enable().removeEvents().setOptions(options);
      }
    });
    Modalbox.getInstance().show(this.element, { onHide: this.bound.modalHide });
  },

  close: function() {
    Modalbox.getInstance().hide();
  },

  preview: function(item) {
    new Swiff(item.getSwfUrl(), { width: 320, height: 259 }).inject(this.previewContainer.empty());
  },

  modalHide: function() {
    this.previewContainer.empty().set('html', this.initialPreviewHtml);
  }

});

/**
 * Represents an items in the LOST item chooser.
 */
Searcher.Lost.Item = new Class({

  Implements: [Options, Events],

  options: {
    item_add_selector: 'img.add-to-list',
    previewer_selector: 'div.item-header',
    rank_selector: 'span.rank',
    data_selector: 'input[name=data]',
    disabled_class: 'disabled'
  },

  initialize: function(element, searcher, options) {
    this.setOptions(options);

    this.bound = {
      add: this.add.bindWithEvent(this),
      preview: this.preview.bindWithEvent(this)
    };

    this.setElement(element);
    this.setSearcher(searcher);

    this.initializeAdderElements();
    this.initializePreviewerElements();
    this.initializeRankElements();
    this.initializeDataElement();
  },

  setElement: function(element) {
    this.element = $(element);
  },

  setSearcher: function(searcher) {
    this.searcher = searcher;
  },

  initializeAdderElements: function() {
    this.adderElements = this.element.getElements(this.options.item_add_selector);
    this.adderElements && this.adderElements.addEvent('click', this.bound.add);
  },

  initializePreviewerElements: function() {
    this.previewerElements = this.element.getElements(this.options.previewer_selector);
    this.previewerElements && this.previewerElements.addEvent('click', this.bound.preview);
  },

  initializeRankElements: function() {
    this.rankElements = this.element.getElements(this.options.rank_selector);
  },

  initializeDataElement: function() {
    this.data = JSON.decode(this.element.getElement(this.options.data_selector).get('value'));
  },

  setRank: function(rank) {
    this.rankElements.set('text', rank);
    return this;
  },

  disable: function() {
    this.element.addClass(this.options.disabled_class).hide();
    return this;
  },

  enable: function() {
    this.element.removeClass(this.options.disabled_class).show();
    return this;
  },

  isEnabled: function() {
    return !this.element.hasClass(this.options.disabled_class);
  },

  add: function(event) {
    event.stop();
    if (this.isEnabled()) {
      Main.searcher.close();
      this.fireEvent('add', this.data);
    }
    return this;
  },

  preview: function(event) {
    event.stop();
    if (this.isEnabled()) {
      this.searcher.preview(this);
      this.fireEvent('preview', this);
    }
    return this;
  },

  existsIn: function(builder) {
    return builder.listItems.get('html').match(this.data.title);
  },

  getSwfUrl: function() {
    return Context.swf+'/mediaplayer.swf?autostart=true&file='+encodeURIComponent(this.data.url);
  }

});

/**
 * Abstraction class for a content piece in the content browser.
 */
var Content = new Class({

  $family: { name: 'content' },
  Extends: Model,

  /**
   * @param {Object|Element} data
   * 
   * @constructor
   */
  initialize: function(data) {
    var that = this;

    this.addField('title', '.title');
    this.addField('description', '.description');
    this.addField('source', 'input[name=source]');
    this.addField('source_key', 'input[name=source_key]');
    this.addField('content_type');
    this.addField('url', 'input[name=url]');
    this.addField('image', 'input[name=image]');
    this.addField('full_result', 'input[name=full_result]');

    // accepts either data or an element
    if ('element' == $type(data)) {
      this.parent(data);
      this.data = this.getValue('full_result').parseQuery();
    } else {
      this.parent(new Element('div', {
        html: Content.template.substitute($extend($unlink(data), { full_result: $H(data).toQueryString(false, false) }))
      }).getFirst());
      this.data = data;
    }

    // element shortcuts
    this.adder       = this.element.getElement('.add-remove');
    this.player      = this.element.getElement('.play');
    this.image       = this.element.getElement('.image-no-link');
    this.linkedImage = this.element.getElement('.image-with-link');

    this.itemContents = [];

    if (this.adder) {
      this.adder.addEvents({
        click: function(event) {
          event.stop();
          that.injectIntoItem();
        }
      });
    }

    if (this.canClickThru()) {
      this.image.hide();
      this.linkedImage.show();
    }

    if (this.player) {
      if (this.isVideo()) {
        this.player.addEvents({
          click: function(event) {
            event && event.stop();
            that.preview(that.adder ? Main.builder.currentItem : false);
          }
        });
      } else {
        this.player.destroy();
      }
    }
  },

  /**
   * Returns true if the current content can be clicked.
   * 
   * @return Boolean
   */
  canClickThru: function() {
    // fixme: key off content type, not source
    return ['amazon', 'alexa', 'yelp'].contains(this.data.source);
  },

  getValues: function() {
    return this.element.getElement('input[name="full_result"]').get('value').parseQuery(true, true);
  },

  /**
   * Returns true if the current content is a video.
   * 
   * @return Boolean
   */
  isVideo: function() {
    return ['youtube', 'disney', 'g4'].contains(this.data.source);
  },

  /**
   * Returns a Swiff object for the current content.
   * 
   * @return Swiff
   */
  getSwiff: function() {
    var url, options = { width: 408, height: 327 };

    switch (this.data.source) {
      case 'youtube':
        url = 'http://www.youtube.com/v/'+this.data.source_key+'&autoplay=1';
        break;
      case 'disney':
        url = Context.swf+'/mediaplayer.swf?autostart=true&file='+encodeURIComponent('http://www.totaleclips.com/Player/Bounce.aspx?eclipid='+this.data.source_key+'&bitrateid=308&vendorid=3&type=.flv');
        break;
      case 'g4':
        url = Context.swf+'/mediaplayer.swf?autostart=true&file='+encodeURIComponent(this.data.url);
        break;
      default:
        throw 'Content is not a video';
    }

    return new Swiff(url, options);
  },

  /**
   * Previews the current content in a modal window.
   * 
   * @param  {Item} item Include "add" button if appropriate for this Item
   * @return Content
   */
  preview: function(item) {
    var that = this;
    if (this.isVideo()) {
      var div = new Element('div', {'class': 'module', 'id': 'video_preview_modal'})
      var swfContainer = new Element('div', {'class': 'module-body'}).inject(div), swf = this.getSwiff().inject(swfContainer);
      if (this.element.getParents().contains(Main.searcher.element)) {
        new Element('button', {'class': 'button'}).set('text', 'add to item').addEvent('click', function(event) {
          event.stop();
          Modalbox.getInstance().hide();
          that.injectIntoItem(item);
        }).inject(div);
      }
      Modalbox.getInstance().show(div, { pleaseDestroy: true });
    }
    return this;
  },

  /**
   * Resets the current Content's presentation and draggability for the current item.
   * 
   * @param  {Item} item The item to reset to
   * @return Content
   */
  reset: function(item) {
    var that = this, item = item || Main.builder.currentItem;

    this.element.removeEvents();

    if (this.existsIn(item)) {
      this.element.setStyle('opacity', 0.25).addClass('added');
    } else if (item) {
      this.element.setStyle('opacity', 1).removeClass('added').addEvents({
        mousedown: function(event) {
          event.stop();
          that.element.addEvent('mousemove', function(event) {
            event.stop();
            that.element.removeEvents('mousemove').ghost().makeDraggable({
              droppables: [item.dropZone],
              onDrop: function(drag, drop) {
                drop && that.injectIntoItem(item);
              },
              onComplete: function(drag) {
                // todo: some sexy revert effect...
                drag.destroy();
              }
            }).start(event);
          });
        },
        mouseup: function(event) {
          event.stop();
          that.element.removeEvents('mousemove');
        }
      });
    }

    return this;
  },

  /**
   * Returns the title of this content.
   * 
   * @return String
   */
  getTitle: function() {
    return this.data.name;
  },

  /**
   * Returns true if this Content has already been added to the supplied Item.
   * 
   * @param  {Item} item
   * @return Boolean
   */
  existsIn: function(item) {
    for (var i = 0, l = item && item.itemContents ? item.itemContents.length : 0; i < l; i++)
      if (item.itemContents[i].joins(item, this)) return true;
    return false;
  },

  /**
   * Injects the Content into the Searcher.
   * 
   * @param   {Searcher} searcher
   * @return  Content
   */
  injectIntoSearcher: function(searcher) {
    this.element.inject(searcher.results);
    return this;
  },

  /**
   * Injects the Content element into an Item.
   * 
   * @param  {Item} item
   * @return Content
   */
  injectIntoItem: function(item) {
    item = item || Main.builder.currentItem;
    if (!this.existsIn(item) && item.itemContents.length < 3) new ItemContent(item, this).inject();
    return this;
  }
});

/* ------------------------------------------------------------------------ */

Content.extend({

  instances: new Hash(),

  /**
   * Returns a pooled or new Content instance.
   * 
   * @param  {Object} result
   * @return Content
   */
  getInstance: function(result) {
    // the JSON string acts as a serialized key
    var key = JSON.encode([result.source, result.source_key]), content = Content.instances.get(key);
    content || Content.instances.set(key, content = new Content(result));
    return content;
  },

  /**
   * Returns the sticky window used for content preview.
   * 
   * @return StickyWinFxModal
   */
  getPreviewWindow: function() {
    if (!Content.previewWindow) {
      Content.previewWindow = new StickyWin.Fx.Modal({
        id: 'video_player_modal',
        showNow: false,
        useIframeShim: false,
        width: 480, 
        height: 385,
        onClose: function() { this.win.empty(); }
      });
    }

    return Content.previewWindow;
  }

});

Content.template = Dynamic.contentTemplate;

/**
 * Represents an item + content association.
 */
var ItemContent = new Class({

  $family: { name: 'item_content' },
  Extends: Model,

  /**
   * @param {Item} item
   * @param {Content|Element} content
   * 
   * @constructor
   */
  initialize: function(item, content) {
    var that = this;

    if ('element' == $type(content)) {
      this.parent(content);
      content = new Content(content);
    } else {
      this.parent(content.element.clone());
    }

    this.player = this.element.getElement('.play');

    this.item = item;
    this.item.itemContents.include(this);

    this.content = content;
    this.content.itemContents.include(this);

    // behaviors
    this.element.getElement('.add-remove').addEvent('click', function(event) {
      event.stop();
      that.destroy();
    });

    if (this.player) {
      this.player.addEvents({
        click: function(event) {
          event.stop();
          that.content.preview();
        }
      });
    }
  },

  getValues: function() {
    return this.content.getValues();
  },

  /**
   * Destroys the item/content association and removes content from the DOM.
   * 
   * @return ItemContent
   */
  destroy: function() {
    this.item.itemContents.erase(this);
    this.content.itemContents.erase(this);

    // todo: some sort of sexy dissolve...
    this.element.destroy();

    this.item.fireEvent('contentChange');
    return this;
  },

  /**
   * Injects a clone of the current Content into the current Item.
   * 
   * @return ItemContent
   */
  inject: function() {
    this.element.inject(this.item.dropInstructions, 'before');
    this.item.fireEvent('contentChange');
    return this;
  },

  /**
   * Returns true if the current ItemContent joins the supplied Item and Content.
   * 
   * @return Boolean
   */
  joins: function(item, content) {
    if (
      this.item == item
      &&
      this.content.data.source == content.data.source
      &&
      this.content.data.source_key == content.data.source_key
    ) {
      return true;
    } else {
      return false;
    }
  }
});

var Share = new Class({

  $family: { name: 'share' },

  initialize: function() {
    var that = this;

    this.shareForm            = $('share_form');
    this.shareFormContainer   = $('share_form_container');
    this.versionChooser       = $('share_email_version_chooser')

    this.shareName            = $('share_name');
    this.shareEmail           = $('share_from');

    this.shareTo              = $('share_to');

    this.shareMessage         = $('share_content');
    this.shareSubject         = $('share_subject');

    this.submitButton         = $('share_submit');

    this.shareProgress        = $('share_progress_popup');
    this.shareProgressMessage = $('share_progress_popup_message');
    this.shareProgressDefaultMessage = this.shareProgressMessage ? this.shareProgressMessage.getAttribute('default-message') : null;
    this.shareProgressWait    = $('share_progress_wait');
    this.shareProgressOK      = $('share_progress_ok');

    this.addressImportForm    = $('address_book_get');
    this.addressImportFormContacts = $('address_book_contacts');
    this.addressImportButton  = $('button-import-addresses');
    
    this.contactsForInviteForm    = $('address_book_add');
    this.contactsForInviteButton  = $('address_book_add_submit');

    this.shareAddressBook     = $('share_address_book_popup');
    this.shareProgressPopup   = null;

    this.PROGRESS_STARTED     = 0;
    this.PROGRESS_SUCCESS     = 1;
    this.PROGRESS_FAIL        = 2;

    this.sharePopupWindow     = null;
    this.sharePopupButtons    = $$('.share-popup');

//    this.facebookList = new FacebookList('share_to', 'share_address_book');
    new FormCheck('share_form');

    if (this.sharePopupButtons) {
      this.sharePopupButtons.each(function(element) {
        element.addEvent('click', function(event) {
          event.stop();
          that.showPopup();
        });
      });
    }

    if (this.addressImportButton) {
      this.addressImportButton.addEvent('click', function(event) {
        event.stop();
        that.importAddresses();
      });
    }
    
    if (this.contactsForInviteButton) {
      this.contactsForInviteButton.addEvent('click', function(event) {
        event.stop();
        that.getSelectedAddresses();
      });
    }

    if (this.submitButton) {
      this.submitButton.addEvent('click', function(event){
        event.stop();
        if (that.sharePopupWindow != null) {
          that.sharePopupWindow.hide();
        }
        that.saveListAndShare();
      });
    };

    if (this.shareProgressOK) {
      this.shareProgressOK.addEvent('click', function(event) {
        event.stop();
        that.hideProgress();
      });
    }
  },

  importAddresses: function() {
    new Request.HTML({
      url: this.addressImportForm.get('action'),
      update: this.addressImportFormContacts,
      onFailure: function(xhr) {
        var errors = JSON.decode(xhr.responseText);
        alert($H(errors).getValues().join("\n"));
      }
    }).post(this.addressImportForm);
    
  },

  getSelectedAddresses: function() {
    var selected = [];
    $$('#address_book_contacts input[type=checkbox]').each(function(elem) {
      if (elem.getProperty('checked')) {
        selected.push(elem.get('value').split(':::')[0]);
      }
    })
    if (selected.length > 0) {
      var addresses = selected.join(',');
      this.shareTo.set('value', addresses);
    }
  },

  addFriend: function() {
    // var newFriendEmail = this.addSrc.getProperty('value');
    // this.addSrc.set('value','');
    // // Test to see if the user has entered a new address
    // if(newFriendEmail == this.addSrcWatermark) return;
    // 
    // if(this.emptyMessage.getStyle('display') != 'none') this.emptyMessage.setStyle('display', 'none');
    // if(this.shareBtn && !this.shareBtnActive){
    //   this.shareBtnActive = true;
    //   this.shareBtn.addClass('active');
    // };
    // 
    // var newFriend =  this.addTemplate.clone().removeProperty('id');
    // 
    // newFriend.getElement('strong').setProperty('text', newFriendEmail);
    // newFriend.inject(this.addTarget, 'top');
  },

  showPopup: function() {
    Tracker.trackEvent('/share');
    this.versionChooser.toggleTo(Main.builder.requiresSave() || !Bouncer.getInstance().userIsAnonymous());
    Modalbox.getInstance().show(this.shareFormContainer);
  },

  showProgress: function() {
    // this.setProgressMessage(this.shareProgressDefaultMessage);
    // this.progressPopup = new StickyWinModal({
    //   content: this.shareProgress,
    // 
    //   modalOptions: {
    //     modalStyle:{
    //       'background-color':'#FFF',
    //       'opacity':.8
    //     }
    //   }
    // });
  },

  updateProgress: function(status, msg) {
    this.setProgressMessage(msg);
    if (status == this.PROGRESS_SUCCESS || status == this.PROGRESS_FAIL) {
      this.shareProgressWait.setStyle('display', 'none');
      this.shareProgressOK.setStyle('display', 'block');
    }
  },

  setProgressMessage: function(msg) {
    this.shareProgressMessage.set('text', msg);
  },

  hideProgress: function() {
    this.progressPopup.hide();
  },

  valueOrNull : function(s) {
    return s.trim().length > 0 ? s : null;
  },

  saveListAndShare: function() {
    // save current list first
    var that = this;

    Main.builder.clearChain();
    Main.builder.chain(function() {
      that.shareForm.set('action', Main.builder.getPermalink('lists_send'));
      that.submitForm();
    }).save();
  },

  submitForm: function() {
    var that = this;

    if (this.validate()) {
      this.shareForm.set('send', {
        bouncerOptions: { leaving: true },
        onRequest: function() {
          Modalbox.getInstance().show(new Element('div', { 'class': 'save-share', html: 'sharing...' }), { locked: true });
        },
        onSuccess: function(response) {
          Tracker.trackEvent('/share/success');
          Modalbox.getInstance().show(Main.builder.getNextStepsElement());
        },
        onFailure: function() {
          Tracker.trackEvent('/share/failure');
        }
      }).send();
    }
  },

  resetForm: function() {

    return;

    // $$('.bit-box').dispose();
    // $('share_to').set('value','');
  },

  validate: function() {
    var addr = this.shareTo.get('value');
    if (!addr || addr.length == 0) {
      alert("Please enter at least 1 email address");
      Modalbox.getInstance().hide();
      return false;
    }
    return true;
  },
  
  /**
   * Handles form submission failure.
   */
  failure: function(xhr) {
    var form = this.getCurrentForm(), errors = JSON.decode(xhr.responseText);
    for (var name in errors) {
      var field = form.getElement('input[name$="['+name+']"]'),
        error = new Element('p', { text: errors[name], 'class': 'form-error' });
      field ? field.addClass('form-error').grab(error, 'after') : form.getElement('.error-container').grab(error);
      this.formErrors.push(error);
    }
  }

});

/**
 * Handles overlay of participant rankings in the list explorer.
 */
var Explorer = new Class({

  $family: { name: 'explorer' },
  Implements: Options,

  options: {
    tab_over_class: 'tab-overlay'
  },

  /**
   * @constructor
   */
  initialize: function(builder, options) {
    var that = this;

    this.builder = builder;
    this.setOptions(options);

    this.comparisonTab   = $('tab-comparison-opinion') ? $('tab-comparison-opinion').getElement('span') : null;
    this.comparisonItems = $('tab-comparison-opinion-items');
    this.holdingPen      = $('list_items_holding_pen');

    this.lists = [];

    this.bound = {
      participantEnter:   this.participantEnter.bindWithEvent(this),
      participantLeave:   this.participantLeave.bindWithEvent(this),
      resetHoldingPen:    this.resetHoldingPen.bind(this),
      holdingPenStart:    this.holdingPenStart.bind(this),
      holdingPenInsert:   this.holdingPenInsert.bind(this),
      holdingPenComplete: this.holdingPenComplete.bind(this)
    };

    this.holdingPen && this.initializeHoldingPen();

    $$('.participant-hover').addEvents({
      mouseenter: that.bound.participantEnter,
      mouseleave: that.bound.participantLeave
    });
  },

  /**
   * Initializes the holding pen.
   */
  initializeHoldingPen: function() {
    var that = this;

    // place all items from consensus into holding pen
    this.getConsensusList().items.each(function(data, i, items) {

      // create a new holding pen item
      var item = Item.createInstance(that.builder, { itemClass: Item.HoldingPen, readOnly: true }).hydrate(data);
      var element = item.element.inject(that.holdingPen);
      var clone = new Element('li');
      var drag = element.makeDraggable({
        onStart: that.bound.holdingPenStart,
        onEnter: that.bound.holdingPenInsert,
        onComplete: that.bound.holdingPenComplete
      });

      var coord = element.getCoordinates();
      clone.setStyles({ width: coord.width, height: coord.height });

      element.store('pen:position', { left: coord.left, top: coord.top });
      element.store('pen:clone', clone);
      element.store('pen:item', item);
      element.store('pen:itemData', data);
      element.store('pen:drag', drag);
      element.store('pen:rank', i);
    });

    this.builder.addEvent('item:destroy', this.bound.resetHoldingPen);
    this.resetHoldingPen();
  },

  /**
   * Starts the drag interaction.
   * 
   * @param {Element} element
   */
  holdingPenStart: function(element) {
    this.builder.sortStart(null, element);
  },

  /**
   * Inserts the element's clone according to what was moused-over.
   * 
   * @param {Element} element
   * @param {Element} overed
   */
  holdingPenInsert: function(element, overed) {
    var clone = element.retrieve('pen:clone');
    if (this.builder.listItems == overed) {
      clone.inject(overed).show();
    } else if (this.holdingPen == overed) {
      var rank = element.retrieve('pen:rank');
      clone.hide();
      rank ? clone.inject(this.holdingPen.getChildren()[rank], 'before') : clone.inject(this.holdingPen, 'top');
    } else {
      clone.inject(overed, overed.getAllPrevious().contains(clone) ? 'before' : 'after').show();
    }
  },

  /**
   * Completes the drag interaction.
   * 
   * @param {Element} element
   */
  holdingPenComplete: function(element) {
    var clone = element.retrieve('pen:clone');

    if (clone.getParents().contains(this.builder.listItems)) {
      element.retrieve('pen:item').addToList(clone);
      element.store('dragged', true);
      this.builder.sortComplete(element);
    } else {
      element.retrieve('pen:clone').dispose();
      this.builder.sortComplete();
    }

    // reset position of holding pen element
    element.setStyles(element.retrieve('pen:position'));
  },

  /**
   * Resets the holding pen.
   * 
   * @return Explorer
   */
  resetHoldingPen: function() {
    var that = this, anyShowing = false;

    this.holdingPen.getChildren().each(function(element) {
      var data = element.retrieve('pen:itemData');
      var drag = element.retrieve('pen:drag');

      if (that.isItemInBuilder(data)) {
        element.hide();
      } else {
        element.show();
        anyShowing = true;

        // update drag
        drag.droppables = [that.holdingPen, that.builder.listItems].concat(that.builder.listItems.getChildren());
      }
    });

    this.holdingPen.toggleTo(anyShowing);

    return this;
  },

  /**
   * Returns true if the supplied item exists in the builder.
   * 
   * @return boolean
   */
  isItemInBuilder: function(item) {
    return 'element' == $type(this.builder.listItems.getElement('input[name*="[id]"][value="'+item.id+'"]'));
  },

  /**
   * Participant rollover.
   *
   * @param  {Event} event
   * @return Explorer
   */
  participantEnter: function(event) {
    if (this.canDisplayOverlay()) {
      var input, showConsensus = false, target = $(event.target),
          element = target.hasClass('participant-hover') ? target : target.getParent('.participant-hover'),
          list = (input = element.getElement('input.username')) ? this.getListBy(input.get('value')) : this.getConsensusList();

      if (element.get('id') == 'tab-comparison-opinion') {
        showConsensus = true;
      } else {
        this.comparisonItems.addClass('user-opinion-items');
        $('tab-comparison-opinion').addClass('user-opinion');
      }

      this.builder.currentItem && this.builder.currentItem.close();
      this.setOverlay(list, showConsensus).show();
    }
    return this;
  },

  /**
   * Participant rollout.
   *
   * @param  {Event} event
   * @return Explorer
   */
  participantLeave: function(event) {
    if (this.canDisplayOverlay()) {
      if (this.comparisonItems.hasClass('user-opinion-items')) {
        this.comparisonItems.removeClass('user-opinion-items');
      }
      if ($('tab-comparison-opinion').hasClass('user-opinion')) {
        $('tab-comparison-opinion').removeClass('user-opinion');
      }
      this.clearOverlay().hide();
    }
    return this;
  },

  /**
   * Shows the comparison overlay.
   *
   * @return Explorer
   */
  show: function() {
    this.comparisonItems.show();
  },

  /**
   * Hides the comparison overlay.
   *
   * @return Explorer
   */
  hide: function() {
    this.comparisonItems.hide();
  },

  /**
   * Returns the list by the supplied participant.
   *
   * @param  {String} username
   * @return Object
   */
  getListBy: function(username) {
    if (!this.lists[username]) {
      this.lists[username] = Explorer.lists.find(function(list) { return list.uuid.test(username+'$') });
    }
    return this.lists[username];
  },

  /**
   * Returns the group consensus list.
   *
   * @return Object
   */
  getConsensusList: function() {
    return Explorer.consensusList;
  },

  /**
   * Returns true if the interface can display overlays.
   *
   * @return Boolean
   */
  canDisplayOverlay: function() {
    return !Main.searcher.isOpen();
  },

  /**
   * Sets the overlay.
   *
   * @param  {Object}  list
   * @param  {Boolean} isConsensus
   * @return Explorer
   */
  setOverlay: function(list, isConsensus) {
    this
      .setTabOverlay('{name}’s opinion'.substitute({ name: isConsensus ? 'group' : list.author }))
      .setListOverlay(list);

    return this;
  },

  /**
   * Clears the overlay.
   *
   * @return Explorer
   */
  clearOverlay: function() {
    return this.clearTabOverlay().clearListOverlay();
  },

  /**
   * Renders the overlay for a participant's list.
   *
   * @param  {Object} list
   * @return Explorer
   */
  setListOverlay: function(list) {
    var that = this;

    this.comparisonItems.getChildren().each(function(element, i) {
      if (i >= list.items.length) return;
      that.setItemOverlay(element, i + 1, list.items[i]);
    });

    return this;
  },

  /**
   * Clears the list overlay.
   *
   * @return Explorer
   */
  clearListOverlay: function() {
    return this;
  },

  /**
   * Sets the text of an overlay tab.
   *
   * @param  {String} text
   * @return Explorer
   */
  setTabOverlay: function(text) {
    this.setTextOverlay(this.comparisonTab, text).addClass(this.options.tab_over_class);
    return this;
  },

  /**
   * Clears the tab overlay.
   *
   * @return Explorer
   */
  clearTabOverlay: function() {
    this.clearTextOverlay(this.comparisonTab).removeClass(this.options.tab_over_class);
    return this;
  },

  /**
   * Populates an overlay item with data.
   *
   * @param  {Element} element
   * @param  {Number}  rank
   * @param  {Object}  item
   * @return Explorer
   */
  setItemOverlay: function(element, rank, item) {
    if (item.image) element.getElement('.item-image').set('src', item.image);
    element.getElement('.rank').set('text', rank);
    element.getElement('.title').set('text', item.title);
    element.show();

    return this;
  },

  /**
   * Utility method for setting the text of an overlay element.
   *
   * @param  {Element} element
   * @param  {String}  text
   * @return Element
   */
  setTextOverlay: function(element, text) {
    if (!element.retrieve('default-text')) {
      element.store('default-text', element.get('text'));
    }
    return element.set('text', text);
  },

  /**
   * Utility method for clearing the text of an overlay element.
   *
   * @param  {Element} element
   * @return Element
   */
  clearTextOverlay: function(element) {
    return element.set('text', element.retrieve('default-text'));
  }

});

Explorer.lists = Dynamic.explorerLists;
Explorer.consensusList = Dynamic.explorerConsensus;

var FeaturedLists = new Class({

  template: '<a href="{url}" class="thumb-cluster"> <img src="{image}" alt="{title}" /> <h3>{title}</h3> </a>',

  /**
   * Constructor.
   */
  initialize: function(element) {
    this.element = $(element);
    this.data = $H(Dynamic.featured);

    this.bound = {
      prev: this.prev.bindWithEvent(this),
      next: this.next.bindWithEvent(this)
    };

    $('prev-category').addEvent('click', this.bound.prev);
    $('next-category').addEvent('click', this.bound.next);

    // show the first category
    this.showCategory(this.data.getKeys()[0]);
    this.element.getChildren().setStyle('display', 'block');
  },

  prev: function(event) {
    event && event.stop();
    var categories = this.data.getKeys(), index = categories.indexOf(this.current) - 1;
    this.showCategory(categories[index] || categories.getLast());
  },

  next: function(event) {
    event && event.stop();
    var categories = this.data.getKeys(), index = categories.indexOf(this.current) + 1;
    this.showCategory(categories[index] || categories[0]);
  },

  /**
   * Shows a category.
   * 
   * @param {String} category A category name
   */
  showCategory: function(category) {
    var container = $('explore_list').empty(), widgets = this.data[category];

    this.current = category;

    $('featuredListCategoryType').set('text', category);
    $('featuredListCategoryBlurb').set('text', 'Like to talk about {category}?'.substitute({ category: category }));

    widgets.each(function(widget) {
      new Element('li').set('html', this.template.substitute(widget)).inject(container);
    }.bind(this));

    // show the first widget
    this.showWidget(widgets[0].flashVars);
  },

  /**
   * Shows a widget.
   * 
   * @param {Object} flashVars The widget flashVars
   */
  showWidget: function(flashVars) {
    new Swiff(Context.swf+'/widget/widget.swf', { width: 316, height: 375, vars: flashVars }).inject($('widget').empty());
  }

});

/**
 * Widget publishing interface.
 */
var Publish = new Class({

  $family: { name: 'publish' },

  /**
   * @constructor
   */
  initialize: function() {
    var that = this;

    this.form = $('add_list');
    this.versionChooser = $('publish_version_chooser');
    this.listName = $('list-name').get('text');
    this.topicId = $('topic-id').get('text');
    this.listId = $('list-id').get('text');
    this.topicUrl = $('topic-url').get('text');
    this.listUrl = $('list-url').get('text');
    this.widgetForm = $('widget_update_form');

    this.bound = {
      reset: this.reset.bind(this)
    };

    if (this.form) {
      this.form.getElements('input[name="list[]"]').addEvent('click', this.bound.reset);
    }

    if (this.versionChooser) {
      this.versionChooser.getElements('input[type="radio"]').addEvent('click', this.bound.reset);
    }

    this.placeLaunchpad(this.listId, this.listName);

    this.widgetForm.set('send', { onFailure: function() {
      alert('An unexpected error occurred.');
    }});
  },

  /**
   * Resets the widget based on form elements.
   */
  reset: function() {
    var flashvars = $H(Widget.flashVars),
        selected  = $splat(this.form.serialize()['list[]']);

    if ($defined(flashvars.L)) {

      // compressed format (only supports group consensus lists)
      flashvars.set('L', selected.map(function(data) { return decodeURIComponent(data).parseQuery().t; }).join('|'));

    } else {

      var version = $('publish_version_chooser_mine').checked ? 'mine' : 'group',
        lists = 'mine' == version && this.listId ? ('L'+this.listId) : ('T'+this.topicId);
      selected.each(function(data, i) {
        data = decodeURIComponent(data).parseQuery(true, true);
        lists += ',';
        lists += 'mine' == version && data.l ? ('L'+data.l) : ('T'+data.t);
      });

      // post updated list ids to the server and refresh the widget
      this.widgetForm.getElement('input[name*="lists"]').set('value', lists);
      this.widgetForm.get('send').chain(Widget.place).send();
    }
  },

  /**
   * Places the Clearspring launchpad in the DOM.
   */
  placeLaunchpad: function(listId, listName) {
    if ($defined($Launchpad)) {
      var widgetWidth  = $('widget-container').get('width');
      var widgetHeight = $('widget-container').get('height');

      $Launchpad.ShowMenu({
        wid: Publish.clearspringWid,
        userId: '49943273a806a44a',
        // userId: '47d6c2c9bc03c4e5',
        targetElement: 'launchpad-menu',
        source: 'widget-publish',
        widgetName: 'ListSpinner Widget - '+listName+' ('+listId+')',
        widgetWidth: widgetWidth,
        widgetHeight: widgetHeight,
        servicesInclude: [
          'blackplanet', 'eons', 'friendster', 'google', 'livejournal',
          'blogger', 'freewebs', 'hoverspot', 'myyearbook', 'pageflakes',
          'netvibes', 'perfspot', 'typepad', 'vox', 'xanga', 'objectcode'
        ]
      });
    } else dbug.log('Launchpad not defined');
  }
});

Publish.clearspringWid = Dynamic.clearspringWid;

Item.Addable = new Class({

  Extends: Content,

  initialize: function(data) {
    var vars = { title: data.title, description: data.description, image1: data.image };
    vars.image2 = data.contents[1] && data.contents[1].image ? data.contents[1].image : '';
    vars.image3 = data.contents[2] && data.contents[2].image ? data.contents[2].image : '';

    this.data = data;
    this.element = new Element('ul', { html: Item.Addable.template.substitute(vars) }).getFirst();
    this.adder = this.element.getElement('a.add');

    vars.image3 || this.element.getElements('img')[2].destroy();
    vars.image2 || this.element.getElements('img')[1].destroy();

    this.bound = {
      add: this.add.bindWithEvent(this)
    }

    this.adder.addEvent('click', this.bound.add);
  },

  injectIntoSearcher: function(searcher) {
    this.element.inject(searcher.results);
    return this;
  },

  add: function(event) {
    if ('event' == $type(event)) event.stop();
    data = this.data;
    data.use_id = '';
    var item = Item.createInstance(Main.builder).hydrate(data);
    item.element.replaces(Main.builder.currentItem.element);
    Main.builder.currentItem.destroy();
    Main.builder.currentItem = item;

    Main.builder.initializeModifiedEvents(item.element);
    Main.builder.items.push(item);
    Main.builder.rankItems();

    Main.builder.explorer && Main.builder.explorer.resetHoldingPen();
    Main.builder.resetSortable();

    item.open();
  },

  reset: function() { return this; }

});

Item.Addable.template = Dynamic.addableItemTemplate;

var Form = new Class({

  $family: { name: 'Form' },

  Implements: [Events, Options],

  options: {
    baseId:             'formName',
    errorClass:         'error',
    globalErrorsId:     'errors',
    fieldErrorIdSuffix: '-errors'
  },

  initialize: function(options) {
    var that = this;
    this.setOptions(options);
    $(this.options.form).addEvent('submit', function(event) {
      event.stop();
      that.submit();
    });
  },

  displayErrors: function(errors) {

  },

  validate: function() {
    return null;
  },

  submit: function() {

  }

});


var JSONServiceResponse = new Class({
  Implements: [Options],

  options: {
    errorsKey:          'errors',
    globalErrorsSubkey: 'global',
    fieldErrorsSubkey:  'field',
    resultsKey:         'results'
  },

  initialize: function(json, options) {
    this.json = json;
    this.setOptions(options);
  },

  isError: function() {
    return this.json && 
      this.json[this.options.errorsKey] &&
      (this.json[this.options.errorsKey][this.options.globalErrorsSubkey].length > 0 ||
       this.json[this.options.errorsKey][this.options.fieldErrorsSubkey])
      ;
  },

  getGlobalErrors: function() {
    return this.json[this.options.errorsKey][this.options.globalErrorsSubkey];
  },

  getFieldErrors: function() {
    return this.json[this.options.errorsKey][this.options.fieldErrorsSubkey];
  },

  getResults: function() {
    return this.json[this.options.resultsKey];
  }
});

/**
 * JSON response format:
 *
 *  errors: [{'global':['msg1','msg2]},{'field1':'message1'},{'field2':'message2',...}],
 *  results: {}
 *
 */
var AjaxForm = new Class({

  $family: { name: 'AjaxForm' },

  Extends: Form,

  options: {
    onRequest: $empty,
    onComplete: $empty,
    onCancel: $empty,
    onSuccess: $empty,
    onFailure: $empty,
    onException: $empty,
    renderGlobalErrors: true
  },

  initialize: function(options) {
    var that = this;
    this.setOptions(options);

    this.waiter = null;
    $(this.options.form).addEvent('submit', function(event) {
      event.stop();
      that.submit();
    });
  },

  makeFieldId: function(fieldName) {
    return this.options.baseId + "_" + fieldName;
  },

  makeErrorFieldId: function(fieldId) {
    return fieldId + this.options.fieldErrorIdSuffix;
  },

  renderErrors: function(globalErrors, fieldErrors) {
    if (globalErrors && this.options.renderGlobalErrors) {
      var globalErrorStr = globalErrors.join("<br>");
      $(this.makeErrorFieldId(this.options.baseId)).
        set('text', globalErrorStr).show();
    }
    if (fieldErrors) {
      for (var key in fieldErrors) {
        //dbug.log(key + " " + fieldErrors[key]);
        $(this.makeErrorFieldId(this.makeFieldId(key))).
          set('text', fieldErrors[key]).show();
      }
    }
  },

  clearErrors: function() {
    $(this.options.form).getElements('.' + this.options.errorClass).set('text','').hide();
  },

  setWaiter: function(isWaiting) {
    if (isWaiting) {
      this.waiter = new Waiter($(this.options.form), {layer: {styles: {zIndex: 11000}}}).start();
    }
    else {
      this.waiter.stop();
    }
  },

  submit: function() {
    var that = this;

    this.clearErrors();
    var errors = this.validate();
    if (errors != null) {
      displayErrors(errors);
    }
    else {
      new Request.JSON({
        url: $(this.options.form).get('action'),
        onRequest: function() {
          that.setWaiter(true);
          that.fireEvent("request");
        },
        onComplete: function() {
          that.setWaiter(false);
          that.fireEvent("complete");
        },
        onCancel: function() {
          that.setWaiter(false);
          that.fireEvent("cancel");
        },
        onSuccess: function(json) {
          that.setWaiter(false);
          var response = new JSONServiceResponse(json);
          if (response.isError()) {
            that.renderErrors(response.getGlobalErrors(), response.getFieldErrors());
          }
          else {
            that.fireEvent("success", json);
          }
        },
        onFailure: function(xhr) {
          that.setWaiter(false);
          that.fireEvent("failure", xhr);
        },
        onException: function(headerName, value) {
          that.setWaiter(false);
          that.fireEvent("exception", headerName, value);
        }
      }).post($(this.options.form));
    }
  }
});
var SendInvitationForm = new Class({

  $family: { name: 'SendInvitationForm' },

  Extends: AjaxForm,

  options: {
  },

  initialize: function(options) {
    options = $extend({
      onSuccess: this.onSuccess,
      onRequest: this.onRequest
    }, options || { });

    this.parent(options);
//    $(this.options.defaultFocus).focus();
  },
  
  onRequest: function() {
    dbug.log("Request sent");
  },

  onSuccess: function(json) {
    dbug.log("Success!");
    dbug.log(json);
  }
})

/**
 *
 */
var ContactsImport = new Class({

  $family: { name: 'ContactsImport' },

  Extends: AjaxForm,

  options: {
  },

  initialize: function(options) {
    var that = this;
    options = $extend({
      onSuccess: this.onSuccess,
      onRequest: this.onRequest
    }, options || { });

    this.parent(options);
//    $(this.options.defaultFocus).focus();
    this.resultsContainer = $(this.options.resultsDiv).getElement('ul');
    this.resultsDoneButton = $(this.options.resultsDiv).getElement('input[type=button]');
    $(this.options.trigger).addEvent('click', function(event) {
      /* display login form */
      event.stop();
      that.resetState();
      that.options.formDialog.position();
      that.options.formDialog.show();
    });
    this.resultsDoneButton.addEvent('click', function() {
      var addresses = that.getSelectedAddresses();
      if (addresses && addresses.length > 0) {
        /* send data back */
        $(that.options.saveContactsTo).set('value', addresses.join(', '));      
      }
      that.options.formDialog.hide();
    });
  },

  resetState: function() {
    this.clearErrors();
    $(this.options.form).reset();
    this.resetResultsContainer();
    this.showLoginDiv();
  },

  showResultsDiv: function() {
    $(this.options.form).hide();
    $(this.options.resultsDiv).show();
  },

  showLoginDiv: function() {
    $(this.options.resultsDiv).hide();
    $(this.options.form).show();
  },

  onRequest: function() {
    dbug.log("Request sent");
  },
  
  onSuccess: function(json) {
    var that=this;
    this.resetResultsContainer();
    json.results.each(function(item) {
      that.addContactRow(item.email);
    });
    this.showResultsDiv();
  },

  addContactRow: function(addr) {
    var rowTemplate = '<input type="checkbox" value="{addr}"> {addr}';
    new Element('li').set('html', rowTemplate.substitute({addr: addr})).inject(this.resultsContainer);
  },

  resetResultsContainer: function() {
    this.resultsContainer.empty();
  },

  getSelectedAddresses: function() {
    var selected = [];
    this.resultsContainer.getElements('input[type=checkbox]').each(function(elem) {
      if (elem.getProperty('checked')) {
        selected.push(elem.get('value'));
      }
    });
    return selected;
  }

});
/* ------------------------------------------------------------------------
 * Main.js
 * 
 * Page-aware initializations and singleton factory.
 * SVN: $Id: Main.js 4320 2009-03-12 00:16:11Z kwallsmith $
 * ------------------------------------------------------------------------ */

var Main = {

  setup: function() {
    var body = $(document.body);

    Main.setupJavascriptLinks();

    // global
    Main.storage = new Hash.Cookie('LISTSPINNER_VIEW', { path: '/', duration: 365 }); 
    Main.branding = new Branding(BrandingKey);
    
    if ($('bouncer')) {
      Main.bouncer = Bouncer.getInstance();      
    }

    if (body.hasClass('builder')) {
      dbug.log('Initializing "builder" view.');

      Main.helpDialogs = new HelpDialogs();
      Main.builder = Main.branding.getBuilder();
      Main.searcher = Main.branding.getSearcher();

      Main.share = new Share();

      Watermark.setup();
    }

    if (body.hasClass('home')) {
      dbug.log('Initializing "home" view.');
      Main.featured = new FeaturedLists('home_slab');
    }

    // initialize publish before widget
    if (body.hasClass('publish')) {
      dbug.log('Initialize "publish" view.');
      Main.publish = Main.branding.getPublish();
    }

    if (body.hasClass('widget')) {
      dbug.log('Initialize "widget" view.');
      Widget.place();
    }

    if (body.hasClass('login')) {
      dbug.log('Initialize "login" view');
      Main.login = new Login();
    }

    Main.helps = new Helps('helps_toggler');
  },

  setupJavascriptLinks: function(container) {
    $(container || document.body).getElements('a[href$="#"]').addEvent('click', function(event) { event.preventDefault(); });
  }

};

Main.setup();

