//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: 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: 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);
	},

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

});

/* Clientcide Plugins (http://www.clientcide.com) */

/*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();
	}*/

	Element.implement({

		expose: function(){
			if (this.getStyle('display') != 'none') return $empty;
			var before = {};
			var styles = { visibility: 'hidden', display: 'block', position:'absolute' };
			//use this method instead of getStyles 
			$each(styles, function(value, style){
				before[style] = this.style[style]||'';
			}, this);
			//this.getStyles('visibility', 'display', 'position');
			this.setStyles(styles);
			return (function(){ this.setStyles(before); }).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);
		}
	});

	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;
		}
	});

	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);
			tabsi = $$(opt.tabs);
			if (tabsi[0] && tabsi[0].retrieve('tabSwapper')) return tabsi[0].retrieve('tabSwapper');
			clickers = $$(opt.clickers);
			tabsi.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;
			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;
		}
	});

	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);
			});
		}
	});

	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")) || [];
		}
	});















