/*  Script: Waiter.js
		Adds a semi-transparent overlay over a dom element with a spinnin ajax icon.
		
		Author:
		Aaron Newton - aaron [dot] newton [at] cnet [dot] com
		
		Class: Waiter
		Adds a semi-transparent overlay over a dom element with a spinnin ajax icon.
		
		Arguments:
		target - (dom element or id) the dom element to overlay; defaults to document.body
		options - (object) a key/value set of options
		
		Options:
		baseHref - (string) url prefix for the img src (see below); defaults to 
				'http://www.cnet.com/html/rb/assets/global/waiter/'
		img - (object) options for the image (see below)
		imgPosition - (object) options passed to <Element.setPosition> for the image; relativeTo is set to the target above automatically.
		layer - (object) options for the overlay layer (see below)
		fxOptions - (object) options passed to the effects used to transition the overlay and the image opacity
		
		img options:
		Properties passed to the element instantiation.
		(start code)
		//default values
		img: {
			src: 'waiter.gif',
			id: 'waitingImg',
			styles: {
				position: 'absolute',
				width: 24,
				height: 24,
				display: 'none',
				opacity: 0,
				zIndex: 999
			}
		}
		(end)
		
		layer options:
		Properties passed to the element instantiation.
		(start code)
		//default values
		layer:{
			id: 'waitingDiv',
			background: '#fff',
			opacity: 0.8
		}
		(end)
		

*/
var Waiter = new Class({
	options: {
		baseHref: '',
		img: {
			src: '../_images/ajax-loader.gif',
			id: 'waitingImg',
			styles: {
				position: 'absolute',
				width: 126,
				height: 22,
				display: 'none',
				opacity: 0,
				zIndex: 999
			}
		},
		imgPosition: {},
		layer:{
			id: 'waitingDiv',
			background: '#000000',
			opacity: 0.7
		},
		fxOptions: {}
	},
	
	initialize: function(target, options){
		this.target = $(document.body);
		this.setOptions(options);
		this.waiterImg = $(this.options.img.id) || new Element('img', $merge(this.options.img, {
			src: this.options.baseHref + this.options.img.src
		})).injectInside(document.body);
		this.waiterDiv = $(this.options.layer.id) || new Element('div', {
			id: this.options.layer.id,
			styles: {
				width: 0,
				height: 0,
				position: 'absolute',
				zIndex: 998,
				display: 'none',
				opacity: 0,
				backgroundColor: this.options.layer.background
			}
		}).injectInside(document.body);
		this.waiterFx = this.waiterFx || new Fx.Elements($$(this.waiterImg, this.waiterDiv), this.options.fxOptions);
	},
/*  Property: toggle
		Toggles the waiter over the specified element. If the waiter is currently showing over the specified element, it will hide. Otherwise it will 
	*/
	toggle: function(element, show) {
		if (this.inTransit) {
			this.chain(this.toggle.bind(this, [element, show]));
			return this;
		}
		//the element or the default
		element = $(element) || $(this.active) || $(this.target);
		if (!$(element)) return this;
		if (this.active && element != this.active) return this.stop().chain(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;
	},
	start: function(element){
		if (this.inTransit) {
			this.chain(this.start.bind(this, element));
			return this;
		}
		this.inTransit = true;
		element = $(element) || $(this.target);
		var start = function() {
			var dim = element.getComputedSize();
			this.active = element;
			this.waiterImg.setPosition($merge(this.options.imgPosition, {
				relativeTo: element
			})).show();
			this.waiterDiv.setStyles({
				width: dim.totalWidth,
				height: dim.totalHeight,
				display: 'block'
			}).setPosition({
				relativeTo: element,
				position: 'upperLeft'
			});
			this.waiterFx.start({
				0: { opacity:[1] },
				1: { opacity:[this.options.layer.opacity]}
			}).chain(function(){
				this.inTransit = false;
				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.inTransit) {
			this.chain(this.stop.bind(this, callback));
			return this;
		}
		if (!this.active) return this;
		this.inTransit = true;
		//fade the waiter out
		this.waiterFx.start({
			0: { opacity:[0]},
			1: { opacity:[0]}
		}).chain(function(){
			this.inTransit = false;
			this.active = null;
			this.waiterDiv.hide();
			this.waiterImg.hide();  
			this.fireEvent('onHide', this.active);
			this.callChain();
			if ($type(callback) == "function") callback.attempt();
		}.bind(this));
		return this;
	}
});
Waiter.implement(new Options, new Events, new Chain);

/*  Class: Ajax
		Extends functionality from <Waiter> into <Ajax>.
		
		Additional Options:
		useWaiter - (boolean) if true will automatically apply a <Waiter> to the update target; defaults to false. Note: if you do not specify a value for update option this is ignored.
		waiterOptions - (object) options value passed on to <Waiter> class.
	*/
if (typeof Ajax != "undefined") {
	var Ajax = Ajax.extend({
		options: {
			useWaiter: false,
			waiterOptions: {}
		},
		initialize: function(url, options){
			this.parent(url, options);
			if (this.options.useWaiter && this.options.update) {
				this.waiter = new Waiter(this.options.update, this.options.waiterOptions);
				this.addEvent('onComplete', this.waiter.stop.bind(this.waiter));
				this.addEvent('onFailure', this.waiter.stop.bind(this.waiter));
			}
		},
		request: function(data) {
			if (this.waiter) this.waiter.start().chain(this.parent.bind(this, data));
			else this.parent(data);
			return this;
		}
	});
}

/*  Script: element.cnet.js
Extends the <Element> object.

Dependancies:
	 mootools - <Moo.js>, <String.js>, <Array.js>, <Function.js>, <Element.js>, <Dom.js>

Author:
	Aaron Newton, <aaron [dot] newton [at] cnet [dot] com>
	
Class: Element
		This extends the <Element> prototype.
	*/
Element.extend({
/*  Property: getDimensions
		Returns width and height for element; if element is not visible the element is
		cloned off screen, shown, measured, and then removed.
		
		Returns:
		An object with .width and .height defined as integers.
		
		Example:
		>$(id).getDimensions()
		> > {width: #, height: #}
	*/
	getDimensions: function() {
		var w = 0;
		var h = 0;
		try { //safari sometimes crashes here, so catch it
			w = this.getStyle('width').toInt();
			h = this.getStyle('height').toInt();
		}catch(e){}
		if((w == 0 || $type(w) != 'number')||(h == 0 || $type(h) != 'number')){
			var holder = new Element('div').setStyles({
				'position':'absolute',
				'top':'-1000px',
				'left':'-1000px'
			}).injectAfter(this);
			var clone = this.clone().injectInside(holder).show();
			w = clone.offsetWidth;
			h = clone.offsetHeight;
			holder.remove();
		}
		return {width: w, height: h, x: w, y: h};
	},
/*  Property: setPosition
		Sets the location of an element relative to another (defaults to the document body).
		
		Note:
		The element must be absolutely positioned (if it isn't, this method will set it to be);
		
		Arguments:
		options - a key/value object with options
		
		Options:
		relativeTo - (element) the element relative to which to position this one; defaults to document.body.
		position - (string) the aspect of the relativeTo element that this element should be positioned. Options are 'upperRight', 'upperLeft', 'bottomLeft', 'bottomRight', and 'center' (the default). With the exception of center, all other options will make the upper right corner of the positioned element = the specified corner of the relativeTo element. 'center' will make the center point of the positioned element = the center point of the relativeTo element.
		offset - (object) x/y coordinates for the offset (i.e. {x: 10, y:100} will move it down 100 and to the right 10). Negative values are allowed.
		smoothMove - (boolean) move the element to the new position using <Fx.Styles>; defaults to false.
		effectOptions - (object) options object for <Fx.Styles>, optional
		returnPos - (boolean) don't move the element, but instead just return the position object ({top: '#px', left: '#px'}); defaults to false
		
	*/
	setPosition: function(options){
		options = $merge({
			relativeTo: document.body,
			position: 'center',
			offset: {x:0,y:0},
			smoothMove: false,
			effectOptions: {},
			returnPos: false
		}, options);
		this.setStyle('position', 'absolute');
		var rel = $(options.relativeTo);
		var top = (rel == document.body)?window.getScrollTop():rel.getTop();
		if (top < 0) top = 0;
		var left = (rel == document.body)?window.getScrollLeft():rel.getLeft();
		if (left < 0) left = 0;
		var dim = this.getDimensions();
		var pos = {};
		var prefY = options.offset.y.toInt();
		var prefX = options.offset.x.toInt();
		switch(options.position) {
			case 'upperLeft':
				pos = {
					'top':(top + prefY) + 'px',
					'left':(left + prefX) + 'px'
				};
				break;
			case 'upperRight':
				pos = {
					'top':(top + prefY) + 'px',
					'left':(left + prefX + rel.offsetWidth) + 'px'
				};
				break;
			case 'bottomLeft':
				pos = {
					'top':(top + prefY + rel.offsetHeight) + 'px',
					'left':(left + prefX) + 'px'
				};
				break;
			case 'bottomRight':
				pos = {
					'top':(top + prefY + rel.offsetHeight) + 'px',
					'left':(left + prefX + rel.offsetWidth) + 'px'
				};
				break;
			default: //center
				var finalTop = top + (((rel == document.body)?window.getHeight():rel.offsetHeight)/2) - (dim.height/2) + prefY;
				var finalLeft = left + (((rel == document.body)?window.getWidth():rel.offsetWidth)/2) - (dim.width/2) + prefX;
				pos = {
					'top': ((finalTop >= 0)?finalTop:0) + 'px',
					'left': ((finalLeft >= 0)?finalLeft:0) + 'px'
				};
				break;
		}
		if(options.returnPos) return pos;
		if(options.smoothMove && this.effects) this.effects(options.effectOptions).start(pos);
		else this.setStyles(pos);
		return this;
	},  
	/*  Property: getComputedSize
		Calculates the size of an element including the width, border, padding, etc.
		
		Arguments:
		options - an object with key/value options
		
		Options:
		styles - (array) the styles to include in the calculation; defaults to ['padding','border']  
		plains - (object) an object with height and width properties, each of which is an 
							array including the edges to include in that plain. 
							defaults to {height: ['top','bottom'], width: ['left','right']}
		mode - (string; optional) limit the plain to 'vertical' or 'horizontal'; defaults to 'both'
		
		Returns:
		size - an object that contans dimension values (integers); see list below
		
		
		Dimension Values Returned:
		width - the actual width of the object (not including borders or padding)
		height - the actual height of the object (not including borders or padding)
		border-*-width - (where * is top, right, bottom, and left) the width of the border on that edge
		padding-* - (where * is top, right, bottom, and left) the width of the padding on that edge
		computed* - (where * is Top, Right, Bottom, and Left; e.g. computedRight) the width of all the 
			styles on that edge computed (so if options.styles is left to the default padding and border,
			computedRight is the sum of border-right-width and padding-right)
		totalHeight - the total sum of the height plus all the computed styles on the top or bottom. by
			default this is just padding and border, but if you were to specify in the styles option
			margin, for instance, the totalHeight calculated would include the margin.
		totalWidth - same as totalHeight, only using width, left, and right

		Example:
(start code)
$(el).getComputedSize();
returns:
{
	padding-top:0,
	border-top-width:1,
	padding-bottom:0,
	border-bottom-width:1,
	padding-left:0,
	border-left-width:1,
	padding-right:0,
	border-right-width:1,
	width:100,
	height:100,
	totalHeight:102,
	computedTop:1,
	computedBottom:1,
	totalWidth:102,
	computedLeft:1,
	computedRight:1
}
(end)    
	*/
	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.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 $merge(styles, size);
	},
	/*  Property: smoothHide
		Transitions the height, opacity, padding, and margin (but not border) from their current height to zero, then set's display to none and resets the          height, opacity, etc. back to their original values.

		Arguments:
		effectOptions - options object to be passed along to Fx.Styles.
	*/
	smoothHide: function(effectOptions){
		var styles = this.getStyles('padding-top', 'padding-bottom', 'margin-top', 
			'margin-bottom', 'border-top-width', 'border-bottom-width');
		styles.height = this.offsetHeight-styles['padding-top'].toInt()-styles['padding-bottom'].toInt()-
			styles['border-top-width'].toInt()-styles['border-bottom-width'].toInt()+"px";
		var zero = {height: '0px', opacity: 0};
		$each(styles, function(style, index, name){ zero[name] = 0; });
		this.effects(effectOptions||{}).start(zero).chain(function(){
			this.setStyles(styles).setStyle('display','none');
		}.bind(this));  
	},
	/*  Property: smoothShow
		Sets the display of the element to opacity: 0 and display: block, then transitions the height, opacity, padding, and margin (but not border) from zero      to their proper height.
		
		Arguments:
		effectOptions - options object to be passed along to Fx.Styles.
		heightOverride - transition to this heigh instead of the offsetHeight of the element.
	*/
	smoothShow: function(effectOptions, heightOverride){
		if(this.getStyle('display') == "none" || 
			 this.getStyle('visiblity') == "hidden" || 
			 this.getStyle('opacity')==0){
			//toggle display, but hide it
			this.setStyles({ 'display':'block', 'opacity':0 });
			var h = heightOverride || this.offsetHeight;
			var styles = Object.extend({opacity: 1},
				this.getStyles('padding-top', 'padding-bottom', 'margin-top', 
					'margin-bottom', 'border-top-width', 'border-bottom-width'));
			styles.height = h-styles['padding-top'].toInt()-styles['padding-bottom'].toInt()-
				styles['border-top-width'].toInt()-styles['border-bottom-width'].toInt()+"px";
			var zero = { height: '0px', opacity: 0 };
			$each(styles, function(style, index, name){ zero[name] = 0; });
			this.setStyles(zero).effects(effectOptions||{}).start(styles);
		}
	}
});