/*
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: 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.
*/

(function(){

	var old = Fx.prototype.initialize;

	Fx.prototype.initialize = function(options){
		old.call(this, options);
		var trans = this.options.transition;
		if (typeof trans == 'string' && (trans = trans.split(':'))){
			var base = Fx.Transitions;
			base = base[trans[0]] || base[trans[0].capitalize()];
			if (trans[1]) base = base['ease' + trans[1].capitalize() + (trans[2] ? trans[2].capitalize() : '')];
			this.options.transition = base;
		}
	};

})();

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 = - Math.pow((11 - 6 * a - 11 * p) / 4, 2) + b * b;
				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: Fx.Marquee.js
	Defines Fx.Marquee, a marquee class for animated notifications.

License:
	http://clientside.cnet.com/wiki/cnet-libraries#license
*/
Fx.Marquee = new Class({
	Extends: Fx.Morph,
	options: {
		mode: 'horizontal', //or vertical
		message: '', //the message to display
		revert: true, //revert back to the previous message after a specified time
		delay: 5000, //how long to wait before reverting
		cssClass: 'msg', //the css class to apply to that message
		showEffect: { opacity: 1 },
		hideEffect: {opacity: 0},
		revertEffect: { opacity: [0,1] },
		currentMessage: null
/*	onRevert: $empty,
		onMessage: $empty */
	},
	initialize: function(container, options){
		container = $(container); 
		var msg = this.options.currentMessage || (container.getChildren().length == 1)?container.getFirst():''; 
		var wrapper = new Element('div', {	
				styles: { position: 'relative' },
				'class':'fxMarqueeWrapper'
			}).inject(container); 
		this.parent(wrapper, options);
		this.current = this.wrapMessage(msg);
	},
	wrapMessage: function(msg){
		if($(msg) && $(msg).hasClass('fxMarquee')) { //already set up
			var wrapper = $(msg);
		} else {
			//create the wrapper
			var wrapper = new Element('span', {
				'class':'fxMarquee',
				styles: {
					position: 'relative'
				}
			});
			if($(msg)) wrapper.grab($(msg)); //if the message is a dom element, inject it inside the wrapper
			else if ($type(msg) == "string") wrapper.set('html', msg); //else set it's value as the inner html
		}
		return wrapper.inject(this.element); //insert it into the container
	},
	announce: function(options) {
		this.setOptions(options).showMessage();
		return this;
	},
	showMessage: function(reverting){
		//delay the fuction if we're reverting
		(function(){
			//store a copy of the current chained functions
			var chain = this.$chain?$A(this.$chain):[];
			//clear teh chain
			this.clearChain();
			this.element = $(this.element);
			this.current = $(this.current);
			this.message = $(this.message);
			//execute the hide effect
			this.start(this.options.hideEffect).chain(function(){
				//if we're reverting, hide the message and show the original
				if(reverting) {
					this.message.hide();
					if(this.current) this.current.show();
				} else {
					//else we're showing; remove the current message
					if(this.message) this.message.dispose();
					//create a new one with the message supplied
					this.message = this.wrapMessage(this.options.message);
					//hide the current message
					if(this.current) this.current.hide();
				}
				//if we're reverting, execute the revert effect, else the show effect
				this.start((reverting)?this.options.revertEffect:this.options.showEffect).chain(function(){
					//merge the chains we set aside back into this.$chain
					if (this.$chain) this.$chain.combine(chain)
					else this.$chain = chain;
					this.fireEvent((reverting)?'onRevert':'onMessage');
					//then, if we're reverting, show the original message
					if(!reverting && this.options.revert) this.showMessage(true);
					//if we're done, call the chain stack
					else this.callChain.delay(this.options.delay, this);
				}.bind(this));
			}.bind(this));
		}).delay((reverting)?this.options.delay:10, this);
		return this;
	}
});/*
Script: Fx.Move.js
	Defines Fx.Move, a class that works with Element.Position.js to transition an element from one location to another.

License:
	http://clientside.cnet.com/wiki/cnet-libraries#license
*/
Fx.Move = new Class({
	Extends: Fx.Morph,
	options: {
		relativeTo: document.body,
		position: 'center',
		edge: false,
		offset: {x:0,y:0}
	},
	start: function(destination){
		return this.parent(this.element.setPosition($merge(this.options, destination, {returnPos: true})));
	}
});

Element.Properties.move = {

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

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

};

Element.implement({

	move: function(options){
		this.get('move').start(options);
		return this;
	}

});
/*
Script: IconMenu.js
	Defines IconMenu, a simple icon (img) based menu.

License:
	http://clientside.cnet.com/wiki/cnet-libraries#license
*/
var IconMenu = new Class({
	Implements: [Options, Events],
	options: {
		container: document,
		images: ".iconImgs",
		captions: ".iconCaptions",
		removeLinks: false,
		clearLinks: false,
		useAxis: 'x',
		onFocusDelay: 0,
		initialFocusDelay: 250,
		onBlurDelay: 0,
		length: 'auto',
		iconPadding: 1,
		scrollFxOptions: {
			duration: 1800,
			transition: 'cubic:in:out'
		},
		backScrollButtons: '#scrollLeft',
		forwardScrollButtons: '#scrollRight',
		onSelect: function(index, img){
			img.morph({
					'border-top-color': '#00A0C6',
					'border-left-color': '#00A0C6',
					'border-right-color': '#00A0C6',
					'border-bottom-color': '#00A0C6'
			});
		},
		onDeSelect: function(index, img){
			img.morph({
					'border-top-color': '#555',
					'border-left-color': '#555',
					'border-right-color': '#555',
					'border-bottom-color': '#555'
			});
//		onFocus: $empty, //mouseover of target area
//		onBlur: $empty, //mouseout of target area
//		onEmpty: $empty,
//		onRemoveItem: $empty,
//		onRemoveItems: $empty,
//		onScroll: $empty,
//		onPageForward: $empty,
//		onPageBack: $empty,
		}
	},
	imgs: [],
	selected: [],
	initialize: function(options) {
		//set the options
		this.setOptions(options);
		//save a reference to the container
		this.container = $(this.options.container);
		//get the captions from the options
		var captions = ($type(this.options.captions) == "string")
			?this.container.getElements(this.options.captions)
			:this.options.captions;
		//get the images from the options
		var imgs = ($type(this.options.images) == "string")
			?this.container.getElements(this.options.images)
			:this.options.images;
		//loop through each one
		imgs.each(function(img, index) {
			//add it to the menu
			this.addItem(img, captions[index], null);
		}, this);
		
		this.fireEvent('onItemsAdded', this.imgs, 50);
		this.side = (this.options.useAxis == 'x')?'left':'top';
		this.container.setStyle(this.side, this.container.getStyle(this.side).toInt()||0);
		this.onFocusDelay = this.options.initialFocusDelay;
		//set up the events
		this.setupEvents();
	},
	toElement: function(){
		return this.container;
	},
	bound: {
			mouseover: {},
			mouseout: {}
	},
	scrollTo: function(index, useFx){
		//set useFx default to true
		useFx = $pick(useFx, true);
		//get the current range in view
		var currentRange = this.calculateRange();
		//if we're there, exit
		if(index == currentRange.start) return this;
		//get the range for the new position
		var newRange = this.calculateRange(index);
		//if this returns no items, exit
		if(!newRange.elements.length) return this; //no next page! >> Ajax here
		//make sure the container has a position set
		if(this.container.getStyle('position') == 'static') this.container.setStyle('position', 'relative');
		//create the scroll effects if not present already
		if(!this.scrollerFx) 
			this.scrollerFx = new Fx.Tween(this.container, $merge(this.options.scrollFxOptions, {property: this.side, wait: false}));
		//scroll to the new location
		if(useFx) {
			this.scrollerFx.start(-newRange.elements[0].offset).chain(function(){
			//set the index to be this new location
				this.fireEvent('onScroll', [index, newRange]);
			}.bind(this));
		} else {
			//we're not using effects, so just jump to the location
			this.scrollerFx.set(-newRange.elements[0].offset);
			this.fireEvent('onScroll', [index, newRange]);
		}
		this.currentOffset = index;
		return this;
	},
	pageForward: function(howMany){
		var range = this.calculateRange();
		return this.scrollTo(($type(howMany) == "number")?range.start+howMany:range.end);
	},
	pageBack: function(howMany) {
		return this.scrollTo(($type(howMany) == "number")?this.currentOffset-howMany:this.calculateRange(this.currentOffset, true).start);
	},
	addItem: function(img, caption, where) {
		if (caption) {
			img.store('caption', caption);
			caption.store('image', img);
		}
		//figure out where to put it
		where = ($defined(where))?where:this.imgs.length;
		//if we've already got this image in there, remove it before putting it in the right place
		if(this.imgs.contains(img)) this.removeItems([img], true);
		//insert the image and caption into the array of these things
		this.imgs.splice(where, 0, $(img));

		//fix the image if it's png
		if(img.src && img.src.test("$png") && Browser.Engine.trident 
			 && !img.hasClass('fixPNG') && typeof fixPNG != "undefined") fixPNG(img);
		//set up the events for the element
		this.setupIconEvents(img, caption);
		this.fireEvent('onAdd', [img, caption]);
		return this;
	},
	removeItems: function(imgs, useFx){
		var range = this.calculateRange();
		if(!imgs.length) return this;;
		//create a copy; this is because
		//IconMenu.empty passes *this.selected*
		//which we modify in the process of removing things
		//so we must work on a copy of that so we don't change
		//the list as we iterate over it
		imgs = $A(imgs);
		//set the fx default
		useFx = $pick(useFx, true);
		//placeholder for the items we're removing; the effect will
		//only be applied to the dom element that contains the image and the caption
		var fadeItems = [];
		var fadeItemImgs = [];
		//the effect we'll use
		var effect = {
				width: 0,
				'border-width':0
		};
		//an object to store all the copies of the effect; one for each item to be passed
		//to Fx.Elements
		var fadeEffects = {};
		//for items that aren't in the current view, we're not going to use a transition
		var itemsToQuietlyRemove = {
			before: [],
			beforeImgs: [],
			after: [],
			afterImgs: []
		};
				
		//a list of all the icons by index
		var indexes = [];
		//for each image in the set to be removed
		imgs.each(function(image){
			var index = this.imgs.indexOf(image);
			//if the image is visible
			if(index >= range.end) {
				itemsToQuietlyRemove.after.push(image.getParent());
				itemsToQuietlyRemove.afterImgs.push(image);
			} else if(index < range.start) {
				itemsToQuietlyRemove.before.push(image.getParent());
				itemsToQuietlyRemove.beforeImgs.push(image);
			} else {
				//store the parent of the image
				fadeItems.push(image.getParent());
				fadeItemImgs.push(image);
				//copy the effect value for this item
				fadeEffects[fadeItems.length-1] = $merge(effect);
			}
			//remove the reference in the selected array
			//because when it's gone, it won't be selected anymore
			this.selected.erase(image);
			//store the index of where this image was in the menu
			indexes.push(index);
		}, this);
		//for the list of images in the menu that were selected, remove them
		//we didn't do this earlier so we could avoid changing
		//the array while we were working on it
		this.imgs = this.imgs.filter(function(img, index){
			return !indexes.contains(index);
		});
		var removed = [];
		//items page left, remove them, but then we have to update the scroll offset to account
		//for their departure
		if(itemsToQuietlyRemove.before.length) {
			var scrollTo = this.imgs.indexOf(range.elements[0].image);
			itemsToQuietlyRemove.before.each(function(el, index){
				this.fireEvent('onRemoveItem', [el]);
				var img = itemsToQuietlyRemove.beforeImgs[index];
				removed.push(img);
				try {
					el.dispose();
					//scroll to the current offset again quickly
				}catch(e){ dbug.log('before: error removing element %o, %o', el, e); }
			}, this);
			this.scrollTo(scrollTo, false);
		}
		//for items page right, just remove them quickly and quietly
		itemsToQuietlyRemove.after.each(function(el, index){
			this.fireEvent('onRemoveItem', [el]);
			removed.push(itemsToQuietlyRemove.afterImgs[index]);
			try {
				el.dispose(); 
			}catch(e){ dbug.log('after: error removing element %o, %o', el, e); }
		});

		//define a function that removes all the items from the dom
		function clean(range, additionalItems){
			var items = [];
			//then fade out the items that are currently visible
			fadeItems.each(function(el, index){
				this.fireEvent('onRemoveItem', [el]);
				items.push(fadeItemImgs[index]);
				try {
					el.dispose(); 
				}catch(e){ dbug.log('fade: error removing element %o, %o', el, e); }
			}, this);
			items.combine(additionalItems);
			this.fireEvent('onRemoveItems', [items]);
			range = this.calculateRange();
			if(range.elements == 0 && range.start > 0) this.pageBack();
			//if there aren't any items left, fire the onEmpty event
			if(!this.imgs.length) this.fireEvent('onEmpty');
		}
		//if we're using effects, do the transition then call clean()
		if(useFx) new Fx.Elements(fadeItems).start(fadeEffects).chain(clean.bind(this, [range, removed]));
		//else just clean
		else clean.apply(this, [range, removed]);
		return this;
	},
	removeSelected: function(useFx){
		this.removeItems(this.selected, useFx);
		return this;
	},
	empty: function(suppressEvent){
		//placeholder for the effects and items
		var effect = {};
		var items = [];
		//loop through all the images in the icon menu
		this.imgs.each(function(img, index){
			//add the icon container to the list of items to remove
			items.push(img.getParent());
			//create a reference for each one to pass to Fx.Elements
			effect[index] = {opacity: 0};
		});
		//create an instance of Fx.Elements and fade them all out
		new Fx.Elements(items).start(effect).chain(function(){
			//then remove them all instantly
			this.removeItems(this.imgs, false);
			//and fire the onEmpty event
			if(!suppressEvent) this.fireEvent('onEmpty');
		}.bind(this));
		return this;
	},
	selectItem: function(index, select){
		//get the image to select
		var img = this.imgs[index];
		//add or remove the "selected" class
		if($defined(select)) {
			if(select) img.addClass('selected');
			else img.removeClass('selected');
		} else {
			img.toggleClass('selected');
		}
		//if it has the class, then fade the border blue
		if(img.hasClass('selected')){
			//store this image in the index of selected images
			this.selected.push(img);
			this.fireEvent('onSelect', [index, img]);
		} else {
			//else we're deselecting; remove the image from the index of selected images
			this.selected.erase(img);
			this.fireEvent('onDeSelect', [index, img]);
		}
		return this;
	},
	getDefaultWidth: function(){
		//if the user specified a width, just return it
		if($type(this.options.length) == "number") return this.options.length;
		//if, on the other hand, they specified another element than the container
		//to calculate the width, use it
		var container = $(this.options.length);
		//otherwise, use the container
		if(!container) container = this.container.getParent();
		//return the width or height of that element, depending on the axis chosen in the options
		return container.getSize()[this.options.useAxis];
	},
	getIconPositions: function(){
		var offsets = [];
		var cumulative = 0;
		var prev;
		//loop through all the items
		this.imgs.each(function(img, index){
			//we're measuring the element that contains the image
			var parent = img.getParent();
			//get the width or height of that parent using the appropriate axis
			cumulative += (prev)?img.offsetLeft - prev.offsetLeft:0;
			prev = img;
			//var size = parent.getSize().size[this.options.useAxis]
			//store the data
			offsets.push({
				image: img,
				size: parent.getSize()[this.options.useAxis],
				offset: cumulative,
				container: parent
			});
		}, this);
		return offsets;
	},
	calculateRange: function(index, fromEnd){
		if(!this.imgs.length) return {start: 0, end: 0, elements: []};
		index = $pick(index, this.currentOffset||0);
		if(index < 0) index = 0;
		//dbug.trace();
		//get the width of space that icons are visible
		var length = this.getDefaultWidth();
		//get the positions of all the icons
		var positions = this.getIconPositions();
		var referencePoint;
		//if we're paginating forward the reference point is the left edge 
		//of the range is the left edge of the first icon
		//but if we're going backwards, the referencePoint is the left edge of the first icon currently in range
		//the problem is if the user removes the entire last page of icons, then this
		//item no longer exists, so...
		if(positions[index]) {
			//if the item exists, use it
			referencePoint = positions[index].offset;
		} else {
			//else the right edge of the last icon is the reference point
			//the last icon is the container of the last image
			var lastIcon = this.imgs.getLast().getParent();
			var coords = lastIcon.getCoordinates();
			//and the reference point is that icon's width plus it's left offset minus the offset 
			//of the parent (which gets offset negatively and positively for scrolling
			referencePoint = coords.width + coords.left - lastIcon.getParent().getPosition().x;
		}
		//figure out which ones are in range
		var range = positions.filter(function(position, i){
			//if the index supplied is the endpoint
			//then it's in range if the index of the icon is less than the index,
			//and the left side is less than that of the one at the end point
			//and if the left side is greater than or equal to the end point's position minus the length
			if(fromEnd) return i < index && 
				position.offset < referencePoint &&
				position.offset >= referencePoint-length;
			
			//else we go forward...
			//if the item is after the index start and the posision is 
			//less than or equal to the max width defined, include it
			else return i >= index && position.offset+position.size <= length+positions[index].offset;
		});
		//return the data
		return (fromEnd)?{start: index-range.length, end: index, elements: range}
					 :{start: index, end: range.length+index, elements: range};
	},
	inRange: function(index) {
		//calculate the range
		var range = this.calculateRange();
		//return the result
		return index < range.end && index >= range.start;
	},
	setupEvents: function(){
		$(this.options.container).addEvents({
			"mouseleave": function() {
				if(this.inFocus) this.inFocus = null;
				this.imgOut(null, true);
			}.bind(this)
		});
		
		$$(this.options.backScrollButtons).each(function(el){
			el.addEvents({
				click: this.pageBack.bind(this),
				mouseover: function(){ this.addClass('hover'); },
				mouseout: function(){ this.removeClass('hover'); }
			});
		}, this);
		$$(this.options.forwardScrollButtons).each(function(el){
			el.addEvents({
				click: this.pageForward.bind(this),
				mouseover: function(){ this.addClass('hover'); },
				mouseout: function(){ this.removeClass('hover'); }
			});
		}, this);

		$$(this.options.clearLinks).each(function(el){
			el.addEvent('click', this.empty.bind(this));
		}, this);
		$$(this.options.removeLinks).each(function(el){
			el.addEvent('click', this.removeSelected.bind(this));
		}, this);
	},
	imgOver: function(img){
		//set the value of what's in focus to be this image
		this.inFocus = img;
		//clear the overTimeout
		$clear(this.overTimeout);
		//delay for the duration of the onFocusDelay option
		this.overTimeout = (function(){
			this.onFocusDelay = this.options.onFocusDelay;
			//if the user is still focused on the image, fire the onFocus event
			if (this.inFocus == img) this.fireEvent("onFocus", [img, this.imgs.indexOf(img)]);
		}).delay(this.onFocusDelay, this);
	},
	imgOut: function(img, force){
		if(!$defined(img) && force) img = this.prevFocus||this.imgs[0];
		//if the focused image is this one
		if(this.inFocus == img && img) {
			//set it to null
			this.inFocus = null;
			//clear the delay timeout
			$clear(this.outTimeout);
			//wait the duration of the onBlurDelay
			this.outTimeout = (function(){
				this.prevFocus = img;
				//if we're not still focused on this image, fire onBlur
				if (this.inFocus != img || (img == null && force)) this.fireEvent("onBlur", [img, this.imgs.indexOf(img)]);
				if (!this.inFocus) this.onFocusDelay = this.options.initialFocusDelay;
			}).delay(this.options.onBlurDelay, this);
		}
	},
	setupIconEvents: function(img, caption){
		//add the click event
		img.addEvents({
			click: function(e){
				//if the user is holding down control, select the image
				if(e.control) {
					this.selectItem(this.imgs.indexOf(img));
					e.stop();
				}
			}.bind(this)
		});
		img.getParent().addEvents({
			mouseover: this.imgOver.bind(this, img),
			mouseout: this.imgOver.bind(this, img)
		});
	}
});/*
Script: JsonP.js
	Defines JsonP, a class for cross domain javascript via script injection.

License:
	http://clientside.cnet.com/wiki/cnet-libraries#license
*/
var JsonP = new Class({
	Implements: [Options, Events],
	options: {
//	onComplete: $empty,
		callBackKey: "callback",
		queryString: "",
		data: {},
		timeout: 5000,
		retries: 0
	},
	initialize: function(url, options){
		this.setOptions(options);
		this.url = this.makeUrl(url).url;
		this.fired = false;
		this.scripts = [];
		this.requests = 0;
		this.triesRemaining = [];
	},
	request: function(url, requestIndex){
		var u = this.makeUrl(url);
		if(!$chk(requestIndex)) {
			requestIndex = this.requests;
			this.requests++;
		}
		if(!$chk(this.triesRemaining[requestIndex])) this.triesRemaining[requestIndex] = this.options.retries;
		var remaining = this.triesRemaining[requestIndex]; //saving bytes
		dbug.log('retrieving by json script method: %s', u.url);
		var dl = (Browser.Engine.trident)?50:0; //for some reason, IE needs a moment here...
		(function(){
			var script = new Element('script', {
				src: u.url, 
				type: 'text/javascript',
				id: 'jsonp_'+u.index+'_'+requestIndex
			});
			this.fired = true;
			this.addEvent('onComplete', function(){
				try {script.dispose();}catch(e){}
			}.bind(this));
			script.inject(document.head);

			if(remaining) {
				(function(){
					this.triesRemaining[requestIndex] = remaining - 1;
					if(script.getParent() && remaining) {
						dbug.log('removing script (%o) and retrying: try: %s, remaining: %s', requestIndex, remaining);
						script.dispose();
						this.request(url, requestIndex);
					}
				}).delay(this.options.timeout, this);
			}
		}.bind(this)).delay(dl);
		return this;
	},
	makeUrl: function(url){
		var index = (JsonP.requestors.contains(this))?
								JsonP.requestors.indexOf(this):
								JsonP.requestors.push(this) - 1;
		if(url) {
			var separator = (url.test('\\?'))?'&':'?';
			var jurl = url + separator + this.options.callBackKey + "=JsonP.requestors[" +
				index+"].handleResults";
			if(this.options.queryString) jurl += "&"+this.options.queryString;
			jurl += "&"+Hash.toQueryString(this.options.data);
		} else var jurl = this.url;
		return {url: jurl, index: index};
	},
	handleResults: function(data){
		dbug.log('jsonp received: ', data);
		this.fireEvent('onComplete', [data, this]);
	}
});
JsonP.requestors = [];/*	
Script: Jlogger.js
	Enables clientside logging for any client side data or events.
	
License:
	No License; internal CNET code not intended for reuse.
	*/
var Jlogger = new Class({
	Implements: [Options, Events],
	options: {
/*	ctype: false,
		cval: false,
		tag: false,
		element: false,
		event: false,
		useraction: false,
		fireOnce: false,
		onPing: $empty */
	},
	errors: 0,
	fired: false,
	active: true,
	initialize: function(options) {
		var defaults = (typeof PageVars != "undefined")?{
			ontid: PageVars.get('nodeId', 'number'),
			siteId: PageVars.get('siteId', 'number'),
            asId: PageVars.get('assetId', 'number'),
			ptId: PageVars.get('pageType', 'number'),
			edId: PageVars.get('editionId', 'number')
		}:{};
		this.setOptions(defaults, options);
		if(this.options.element == 'window') this.options.element = window;
		this.setup();
	}, 
	setup: function(){
		if(!$(this.options.element)) return;
		var opt = this.options; //saving bytes
		if ($type(opt.tag) && $type(opt.element) && $type(opt.event)){
			//else tag is set, element is set, and event is set, log this info and...
			dbug.log('event observe(element: '+opt.element+', event: '+opt.event+', tag: '+opt.tag+')');
			//if the event == "load" and the observed element is the window, execute the ping immediately
			if(opt.event == 'load' && opt.element == window) opt.executeNow = true;
			//observe the elemnt for the event.
			if(opt.element != window) $(opt.element).addEvent(opt.event, this.ping.bind(this));
			else if(opt.event != 'load') $(opt.element).addEvent(opt.event, this.ping.bind(this));
		}
	},
	//generates the url to ping DW and returns it
	makeURL: function(tag) {
		var url = 'http://dw.com.com/redir?';
		var opt = this.options;//saving bytes
		if($type(opt.ontid)) url+= 'ontid='+opt.ontid+'&';
		if($type(opt.siteId)) url+= 'siteid='+opt.siteId+'&';
				if($type(opt.asId)) url+= 'asId='+opt.asId+'&';
				if($type(opt.ptId)) url+= 'ptId='+opt.ptId+'&';
		if($type(opt.edId)) url+= 'edId='+opt.edId+'&';
		if($type(opt.ctype)) url+= 'ctype='+opt.ctype+'&';
		if($type(opt.cval)) url+= 'cval='+opt.cval+'&';
		if($type(opt.useraction)) url+= 'useraction='+opt.useraction+'&';
		url+= 'tag='+opt.tag+'&destUrl=/i/b.gif';
		//append a date value so that the browser doesn't cache the request
		url+= '&uniquePingId='+new Date().getTime();
		return url;
	},
	ping: function(url, force) {
		//if fireOnce is set and this hasn't yet fired, or fireOnce isn't set or is false, and this observer is active
		//then ping dw
		if (force || (((!this.fired && this.options.fireOnce) || !this.options.fireOnce) && this.active)) {
			//if the url isn't passed in to this function, get it from the makeURL function in this class
						url = ($type(url) != 'string' || url.length == 0)?this.makeURL():url;
						//if the doc is loaded, continue with the ping (doing this before the doc is loaded will break IE)
			window.addEvent('domready', function(){
				new Element('img', {src: url});
				//just creating the img and setting its src will cause the browser to hit the url, you don't
				//need to append it to the DOM
				this.fired = true;
				dbug.log(this.options.tag + ': '+(this.options.event||'')+'\nping: '+url);
				this.fireEvent('onPing');
			}.bind(this));
		}
		return this;
	},
	pingTag: function(tag, force){
		return this.ping(this.makeURL(tag), force);
	},
	stopObserving: function(){
		//turns off this logger
		this.active = false;
		return this;
	},
	startObserving: function(){
		//turns it back on
		this.active = true;
		return this;
	}
});
/*
Script: CNETAPI.js
	Defines CNETAPI and associated classes and methods.

License:
	http://clientside.cnet.com/wiki/cnet-libraries#license
*/
var CNETAPI = new Hash({
	register: function(applicationName, options) {
		CNETAPI.apps[applicationName] = $merge({
			//requestUrl: ...,
			//partTag: ....,
			//partKey: ....,
			apiUrl: 'http://api.cnet.com/restApi/v1.0'
		}, options);
		var key = CNETAPI.apps[applicationName].partKey;
		if (key) CNETAPI.apps[applicationName].partKey = key.toString();
	},
	retrieve: function(applicationName) {
		return CNETAPI.apps[applicationName];
	},
	apps: {},
	Utils: {}
});
CNETAPI.register('default');

CNETAPI.Utils.Base = new Class({
	Implements: [Options, Events],
	options: {
		applicationName: 'default',
		jsonpOptions: {
			data: {
				viewType : 'json'
			}
		},
/*	onComplete: $empty,
		onSuccess: $empty,
		onError: $empty, */
		instantiateResults: false,
		resultClass: null,
		errorPath: 'CNETResponse.Error.ErrorMessage.$'
	},
	//easyToUseObjectBuilder : null,
	//apiObjectBuilder : null,
	//packageResults : null,

	initialize : function(options){
		var data = {};
		this.app = (options && options.applicationName)
							 ?CNETAPI.retrieve(options.applicationName)
							 :CNETAPI.retrieve(this.options.applicationName);
		if (this.app.partKey) data.partKey = this.app.partKey;
		if (this.app.partTag) data.partTag = this.app.partTag;
		this.setOptions($merge({
			jsonpOptions: {
				data: data
			}
		}, options));
	},
	//internal
	//gets the query class; defaults to JSONP
	//url - url to hit for data
	//data - key/value options to pass in the query
	getQuery: function(url, options){
		options.data = options.data||{};
		$each(options.data, function(val, key) { 
			options.data[key] = $type(val)=="string"?unescape(val):val; 
		});
		if (this.app.requestUrl) {
			var qs = Hash.toQueryString(options.data);
			options.data = { cnetApiRequest: escape(url+"?"+qs) };
			url = this.app.requestUrl;
		}
		var j = new JsonP(url||"", options);
		return j;
	},
	//internal
	//attempts to return the title of an object
	//data - the object to inspect for the title
	//key - the key or object to look for the $ property
/*		checkDefined : function (returnObject, data, key){
			if(data[key] && data[key].$) return data[key].$ || "";
			return key.$ || "";
	},	*/
	//internal
	//packages up results into a shallow array of results
	//results - the results from the CNET API
	packer : function(results){
		if($type(results) == "array") results = results.filter(function(result){return result});
		else if (results) results = [results];
		else results = [];
		if(this.options.instantiateResults && this.options.resultClass) {
			return results.map(function(obj){
				return new this.options.resultClass(obj);
			}, this);
		} else {
			return results;
		}
	},
	//internal
	//allows you to get food.fruit.apples.red if you have the string "fruit.apples.red"
	//getMemberByPath(food, "fruit.apples.red")
	getMemberByPath: function(obj, path){
		if (path === "" || path == "top" || !path) return obj;
		var member = obj;
		path.split(".").each(function(p){
			if (p === "") return;
			if (member[p]) member = member[p];
			else member = obj;
		}, this);
		return (member == obj)?false:member;	
	},
	//internal
	//handles returned results from the CNET API
	//obj - the json object returned
	handleApiResults : function(obj, path){
		//deal with server error
		var error = this.getMemberByPath(obj, this.options.errorPath);
		return (error) ? error : this.getMemberByPath(obj, path);
		//if the container is specified
	},
	//internal
	//executes a request to the API service
	//jsonData - object passed to jsonp, merged with the data in this.options.jsonpOptions (view & partner key by default)
	//urlSuffix - suffix added to the api url defined in the options
	//path - path to the desired data in the object returned; ex: CNETResponse.TechProducts.TechProduct
	request: function(jsonData, urlSuffix, path){
		var jsonpOptions = $merge(this.options.jsonpOptions, {
				data : jsonData
		});
		var query = this.getQuery(this.app.apiUrl + urlSuffix, jsonpOptions);
		query.addEvent('onComplete',  function(results){
			results = this.handleApiResults(results, path);
			if ($type(results) == "string") {
				dbug.log('CNET API Error: ', results);
				this.fireEvent('onError', [results, query, this]);
			} else {
				this.fireEvent('onSuccess', [this.packer(results), query, this]);
			}
			this.fireEvent('onComplete', [this.packer(results), query, this]);
		}.bind(this));
		query.request();
		return this;
	},
/*	internal
		Throws a javascript error.
		
		Arguments:
		msg - (string) the message for the user
*/
	throwErr: function(msg){
		// Create an object type UserException
		function err (message)
		{
		  this.message=message;
		  this.name="CNETAPI.Utils Exception:";
		};
		
		// Make the exception convert to a pretty string when used as
		// a string (e.g. by the error console)
		err.prototype.toString = function ()
		{
		  return this.name + ': "' + this.message + '"';
		};
		
		// Create an instance of the object type and throw it
		throw new err(msg);
	}
});


CNETAPI.Object = new Class({
	Implements: [Options, Events, Chain],
	options: {
		applicationName: 'default',
		extraLookupData: {},
		type: ""
/*	onSuccess: $empty,
		onError: $empty, */
	},
	ready: false,
	initialize: function(item, options) {
		this.setOptions(options);
		this.app = CNETAPI.retrieve(this.options.applicationName);
		this.type = this.options.type;
		item = ($type(item) == "array" && item.length == 1)?item[0]:item;
		if (!item) return;
		if($type(item) == "object") this.parseData(item);
		else if ($type(item) == "number") this.get(item);
		return;
	},
/*	Property: get
		Gets an item from the CNETAPI.
		
		Arguments
		id - (integer) the object id of the object	*/
	get: function(id){
		try {
			this.makeLookup().get(id);
		} catch(e){
			var msg = 'Error: error on GET: ';
			dbug.log(msg, e);
			this.fireEvent('onError', msg + e.message);
		}
		return this;
	},
	process: function(obj){
		var data = {};
		$H(obj).each(function(value, key) {
			key = this.cleanKey(key);
			switch ($type(value)) {
				case "array":
					data[key] = value.map(function(v) {
						return this.clean(v, key, key);
					}, this);
					break;
				default:
					data[key] = this.clean(value, key, key);
			};
		}, this);
		return data;
	},
	cleanKey: function(key){
		return ($type(key) == "string" && key.test("^@"))?key.substring(1):key;
	},
	clean: function(value, name, path) {
		switch($type(value)) {
			case "string":
				if(value == "false") value = false;
				if(value == "true") value = true;
				if($chk(Number(value))) value = Number(value);
				return value;
			case "function":
				return value;
			case "array":
				return value.map(function(v, i) {
					return this.clean(v, i, path+'.'+name);
				}, this);
				break;
			default:
				var vhash = $H(value);
				if(value.$ && vhash.length == 1) {
					return value.$;
				} else {
					var cleaned = {};
					vhash.each(function(value, key){
						key = this.cleanKey(key);
						if($type(value) == "object" && value.$ && key.test("url", "i") && value.$.test("restApi")) {
							cleaned.walk = cleaned.walk || {};
							//TODO: implement a follow method
							//cleaned.walk[key] = this.follow.pass([value.$, key, path], this);
						}
						cleaned[key] = this.clean(value, key, path+'.'+name);
					}, this);
					return cleaned;
				}
			}
		return this;
	},
	makeLookup: function(){
		return new CNETAPI.Utils[this.options.type]($merge(this.options.extraLookupData, {
			instantiateResults: false,
			onError: this.handleError.bind(this),
			onSuccess: this.parseData.bind(this),
			applicationName: this.app.applicationName
		}));
	},
	handleError: function(msg){
		this.fireEvent('onError', msg);
	},
	parseData: function(data){
		data = ($type(data) == "array" && data.length == 1)?data[0]:data;
		this.json = data;
		this.data = this.process(data);
		this.ready = true;
		this.callChain();
		this.fireEvent('onSuccess', [this, this.data, this.json]);
	}
});

CNETAPI.TechProduct = new Class({
	Extends: CNETAPI.Object,
	options: {
		type: "TechProduct"
	}
});

CNETAPI.SoftwareProduct = new Class({
	Extends: CNETAPI.Object,
	options: {
		type: "SoftwareProduct"
	},
	getSet: function(id) {
		try {
			this.makeLookup().getSet(id);
		} catch(e){
			var msg = 'Error: error on getSet: ';
			dbug.log(msg, e);
			this.fireEvent('onError', msg + e.message);
		}
		return this;
	}
});

CNETAPI.Category = new Class({
	Extends: CNETAPI.Object,
	options: {
		type: "Category",
		siteId: null
	},
	initialize: function(item, options) {
		this.children = [];
		if (options) this.setSiteId(options.siteId);
		this.parent(item, options);
	},
	setSiteId: function(id) {
		this.setOptions({
			extraLookupData: {
				siteId: $chk(id)?id:this.options.siteId
			}
		});
		return this.options.extraLookupData.siteId;
	},
	getChildren: function(options, data){
		var onSuccess = function(data) {
			this.children = data.map(function(d){
				d.options.siteId = siteId;
				return d;
			});
			this.callChain();
		}.bind(this);
		if (this.data.isLeaf) {
			onSuccess([]);
			return this;
		}
		options = options || {};
		var siteId = this.setSiteId(options.siteId);
		if(!$chk(siteId)) {
			var msg = 'Error: you must supply a site id for category lookups.';
			dbug.log(msg);
			this.fireEvent('onError', msg);
			return null;
		} else if(this.data.id) {
			var util = new CNETAPI.Utils[this.options.type]($merge({
					instantiateResults: true,
					resultClass: CNETAPI.Category,
					applicationName: this.app.applicationName
				}, options)).addEvent('onSuccess', onSuccess);
			util.getChildren(this.data.id, $merge(this.options.extraLookupData, data||{}));
			return this;
		} else {
			return null;
		}
		return this;
	}
});

CNETAPI.NewsStory = new Class({
	Extends: CNETAPI.Object,
	options: {
		type: "NewsStory"
	}
});

CNETAPI.NewsGallery = new Class({
	Extends: CNETAPI.Object,
	options: {
		type: "NewsGallery"
	}
});

CNETAPI.Utils.SearchPaths = {
	TechProduct: "/techProductSearch",
	NewsGallery: "/newsGallerySearch",
	NewsStory: "/newsStorySearch",
	SoftwareProduct: "/softwareProductSearch",
	BlogEntry: "/blogEntrySearch"
};


// Individual Implementations for each API Request type
CNETAPI.Utils.TechProduct = new Class({
	Extends: CNETAPI.Utils.Base,
	options: {
		resultClass: CNETAPI.TechProduct,
		instantiateResults: true,
		searchPath: CNETAPI.Utils.SearchPaths['TechProduct']
	},
	search : function(queryTerm, data){
		return this.request($merge({query: queryTerm}, data), this.options.searchPath, "CNETResponse.TechProducts.TechProduct");
	},
	get : function(id, data){
		return this.request($merge({productId: id}, data), "/techProduct", "CNETResponse.TechProduct");
	},
	getMany: function(ids, data) {
		return this.request($merge({productIds: ids}, data), "/techProduct", "CNETResponse.TechProducts.TechProduct");
	}
});

CNETAPI.Utils.SoftwareProduct = new Class({
	Extends: CNETAPI.Utils.Base,
	options: {
		resultClass: CNETAPI.SoftwareProduct,
		instantiateResults: true,
		searchPath: CNETAPI.Utils.SearchPaths['SoftwareProduct']
	},
	search : function(queryTerm, data){
		return this.request($merge({query: queryTerm}, data), this.options.searchPath, "CNETResponse.SoftwareProducts.SoftwareProduct");
	},
	getSet : function(id, data){
		return this.request($merge({productSetId: id}, data), "/softwareProduct", "CNETResponse.SoftwareProduct");
	},
	get: function(id, data) {
		return this.request($merge({productId: id}, data), "/softwareProduct", "CNETResponse.SoftwareProduct");
	},
	getMany: function(ids, data) {
		return this.request($merge({productIds: ids}, data), "/softwareProduct", "CNETResponse.SoftwareProducts.SoftwareProduct");
	},
	getManySets: function(ids, data) {
		return this.request($merge({productSetIds: ids}, data), "/softwareProduct", "CNETResponse.SoftwareProducts.SoftwareProducts");
	}
});

CNETAPI.Utils.NewsStory = new Class({
	Extends: CNETAPI.Utils.Base,
	options: {
		resultClass: CNETAPI.NewsStory,
		instantiateResults: true,
		searchPath: CNETAPI.Utils.SearchPaths['NewsStory']
	},
	search : function(queryTerm, data){
		return this.request($merge({query: queryTerm}, data), this.options.searchPath, "CNETResponse.NewsStories.NewsStory");
	},
	get: function(id, data){
		return this.request($merge({storyId: id}, data), "/newsStory", "CNETResponse.NewsStory");
	},
	getMany: function(ids, data) {
		return this.request($merge({storyIds: ids}, data), "/newsStory","CNETResponse.NewsStories.NewsStory");
	}
});

CNETAPI.Utils.BlogEntry = new Class({
	Extends: CNETAPI.Utils.Base,
	options: {
		resultClass: CNETAPI.BlogEntry,
		instantiateResults: true,
		searchPath: CNETAPI.Utils.SearchPaths['BlogEntry']
	},
	search : function(queryTerm, data){
		return this.request($merge({query: queryTerm}, data), this.options.searchPath, "CNETResponse.BlogEntries.BlogEntry");
	},
	get: function(id, data){
		return this.request($merge({blogEntryId: id}, data), "/blogEntry", "CNETResponse.BlogEntries.BlogEntry");
	},
	getMany: function(ids, data) {
		return this.request($merge({blogEntryIds: ids}, data), "/blogEntry", "CNETResponse.BlogEntries.BlogEntry");
	}
});

CNETAPI.Utils.NewsGallery = new Class({
	Extends: CNETAPI.Utils.Base,
	options: {
		resultClass: CNETAPI.NewsGallery,
		instantiateResults: true,
		searchPath: CNETAPI.Utils.SearchPaths['NewsGallery']
	},
	search : function(queryTerm, data){
		return this.request($merge({query: queryTerm}, data), this.options.searchPath, "CNETResponse.NewsGalleries.NewsGallery");
	},
	get: function(id, data){
		return this.request($merge({galleryId: id}, data), "/newsGallery", "CNETResponse.NewsGallery");
	},
	getMany: function(ids, data) {
		return this.request($merge({galleryIds: ids}, data), "/newsGallery", "CNETResponse.NewsGalleries.NewsGallery");
	}
});

CNETAPI.Utils.Category = new Class({
	Extends: CNETAPI.Utils.Base,
	options: {
		resultClass: CNETAPI.Category,
		instantiateResults: true,
		siteId: null,
		searchPath: CNETAPI.Utils.SearchPaths['TechProduct']
	},
	packer: function(results){
		results = this.parent(results);
		return results.map(function(cat){
			cat.options.siteId = this.options.siteId;
			return cat;
		}, this);
	},
	get: function(id, data){
		data = data||{};
		data.siteId = data.siteId || this.options.siteId;
		if(!$chk(data.siteId)) {
			dbug.log("You must supply a site id for category lookups");
			this.throwErr("You must supply a site id for category lookups");
		}
		this.options.siteId = data.siteId;
		return this.request($merge({categoryId: id}, data), "/category", "CNETResponse.Category");
	},
	getMany: function(ids, data) {
		return this.request($merge({categoryIds: ids}, data), "/category", "CNETResponse.Categories.Category");
	},
	getChildren: function(id, data){
		data = data||{};
		data.siteId = data.siteId || this.options.siteId;
		if(!$chk(data.siteId)) {
			dbug.log("You must supply a site id for category lookups");
			this.throwErr("You must supply a site id for category lookups");
		}
		return this.request($chk(id)?$merge({categoryId: id}, data):data, "/childCategories", "CNETResponse.ChildCategories.Category");
	},
	search : function(queryTerm, type, data){
		data = $merge({
			results: 1,
			iod:'relatedCats'
		}, data);
		return this.request($merge({query: queryTerm}, data), type||this.options.searchPath, "CNETResponse.RelatedCategories");
	}
});
/*	Script: RTSS.js
		RTSS clientside methods for storing user data.
		
License:
	No license; internal cnet code not meant for reuse.
*/
var RTSS = new Class({
	Implements: [Options, Events],
	options: {
		server: 'http://reviews.cnet.com',
		path: '/9804-4_7-0-0.html',
		verifyCookie: 'XCLGFbrowser',
		cookieRetryDuration: 0,
		retryTimes: 0,
		xhrOptions: {
			method: 'get'
		},
		onRequest: $empty,
		onSuccess: $empty,
		onFailure: $empty
	},
	initialize: function(options){
		this.setOptions(options);
		this.createRemote();
	},
	createRemote: function(){
		var xhr = new Request(this.options.xhrOptions);
		xhr.addEvent('onSuccess', this.handleResponse.bind(this));
		xhr.addEvent('onFailure', this.handleError.bind(this));
		return xhr;
	},
	makeUrl: function(queryString){
		//dbug.log('made url: ', this.options.server+this.options.path+'?'+data);
		return this.options.server+this.options.path+'?'+queryString;
	},
	request: function(url) {
		dbug.log('RTSS sending ', url);
		this.fireEvent('onRequest');
		return this.createRemote().send(url)
	},
	handleResponse: function(response){
		this.fireEvent('onSuccess', response);
		dbug.log('RTSS response: ', response);
	},
	handleError: function(response){
		this.fireEvent('onFailure', response);
		dbug.log('RTSS error: ', response);
	}
});
/*	
Script: RTSS.JsonP.js
	Extends the RTSS class creating a new class that uses <JsonP> as its transport.
		
License:
	No license; internal cnet code not meant for reuse.
	*/
RTSS.JsonP = RTSS.extend({
	options: {
		jsonpOptions: {}
	},
	createRemote: function(){
		var jsonp = new JsonP('', this.options.jsonpOptions);
		jsonp.addEvent('onComplete', this.handleResponse.bind(this));
		return jsonp;
	},
	request: function(url){
		dbug.log('RTSS sending ', url);
		var jsonp = this.createRemote();
		jsonp.request(url);
		this.fireEvent('onRequest');
		return jsonp;
	}
});
/*	
Script: UserHistory.js
		Wraps RTSS, ProductToolbar together (and by proxy IconMenu and Fx.Marquee). Contains logic
		for switching between Embedded and bottom view, cookie storate for prefs, and logic to maintain
		data sets for each included class.

License:
	No License; internal CNET code not intended for reuse.
*/
var UserHistory = new Class({
	Implements:[Options, Events],
	options: {
		data: [],
		cookieName: 'userHistoryBarPrefs',
	   cacheCookieName: 'userHistoryCache',
		cookieOptions: {
			duration: 999,
			path: '/'
		},
		rtssOptions: {
			server: 'http://reviews.cnet.com',
			path: '/9804-4_7-0-0.html',
			defaultRequestParams: {
				_siteid_: 3,
				_editionid_: 3,
				startAt: 0,
				maxHits: 30
			},
			jsonpOptions: {
				retries: 1
			}
		},
		buildNow: true,
		toggleLinks: '#showOnAll, #closeHistoryX, #optionLinks img.remove',
		announce:false,
		overrideVisiblePref: false,
		defaultPosition: 'inpage',
		defaultVisible: false,
		onEmbed: Class.empty,
		onAffix: Class.empty,
		onDataLoad: Class.empty
	},
	initialize: function(options){
		this.setOptions(options);
		this.getPrefs();
		this.verifySessionCookie();
		this.createRTSSManager();
		if(this.options.buildNow) this.createBar({
			announce: (this.prefs.get('visible')||this.options.overrideVisiblePref)?this.options.announce:false
		});
	},
	createBar: function(options){
		options = $merge({
			position: this.prefs.get('docPosition'),
			slideIn: this.prefs.get('visible'),
			useFxForSlide: this.prefs.get('docPosition') == "bottom",
			hidden: false
		}, options);
		//set the new position
		this.position = options.position;
		var hovered = false;
		var hoverTimeout;
		//destroy the old instance if there was one
		if(this.bar) {
			this.bar.destroy();
			this.bar = null;
		};
		//if the bar is at the bottom, create a new bar
		if(this.position == 'bottom') {
			this.bar = new ProductToolbar({
				hidden: options.hidden,
				data: options.data||[]
			});
		//otherwise we're doing an in-page toolbox
		} else {
			//if we're not on a page that has a placeholder for the embedded bar, exit quietly
			if(!$('contentAux') && !$('toolboxHistory')) return;
			//grab or make the place holder for the toolbox
			var tbh = $('toolboxHistory')|| new Element('div', {id: 'toolboxHistory'}).inject($('main-full-r'),'top');
			tbh.addEvent('mouseover', function(){
					hovered = true;
					$clear(hoverTimeout);
					hoverTimeout = (function(){
						if (hovered && PageVars.history.generalHoverLogger) PageVars.history.generalHoverLogger.ping();
					}).delay(200, this);
			}.bind(this));
			tbh.addEvent('mouseout', function(){
					hovered = false;
			});
			//create the embedded bar
			this.bar = new EmbeddedProductToolbar({
				//some of these could be moved up into the options above if they need to be more available...
				injectTarget: tbh,
				hidden: options.hidden,
				slideInPosition: 152,
				data: options.data||[],
				marqueeFxOptions: {
					startingStyle: {left: 41},
					showEffect: {
						left: [-400, 41],
						opacity: [0,1]
					},
					revertEffect: {
						top: [-20, 0],
						opacity: [0,1]
					}
				}
			});
		};
		//set up the toggle links
		$$(this.options.toggleLinks).each(function(el){
			el.addEvent('click', this.toggleLocation.bind(this));
		}, this);
		//create the icon set for the bar
		//this.bar.buildSet();
		//add a slide in event to reset the prefs
		this.bar.addEvents({
			onSlideIn: function(){
				if($('marquee')) $('marquee').addClass('shown');
				if($('userStatus')) $('userStatus').addClass('shown');
				this.prefs.set('visible', true);
			}.bind(this),
			onSlideOut: function(){
				if($('marquee')) $('marquee').removeClass('shown');
				if($('userStatus')) $('userStatus').removeClass('shown');
				this.prefs.set('visible', false);
			}.bind(this)
		});
		
		this.bar.iconMenu.addEvents({
			//when the user empties the icon menu, empty rtss, too
			onEmpty: function(){
				dbug.log('emptying rtss');
				this.rtss.empty();
				this.inBarMessage();
			}.bind(this),
		//when the user scrolls, make sure there are enough items in the menu
		//for the next page over
			onScroll: this.loadNeededItems.bind(this),
		//when the user removes an item or items
			onRemoveItems: function(ids){
				//remove them from rtss
				this.remove(ids);
				//then filter them to make sure they are integers
				ids = ids.map(function(id){ return id.toInt() });
				//then remove them from the data in the ProductToolbar
				this.bar.data.filter(function(item){
					//find each data item that is in the ids array
					return !ids.contains(item.id);
				}).each(function(removed){
					//and remove that item from the data aray
					this.bar.data.remove(removed);
				}, this);
				//update the in-bar messaging to the user
				this.inBarMessage();
			}.bind(this)
		});
	
		//if we're sliding in and not announcing, then, well, slide in...
		if(options.slideIn && !options.announce) this.bar.slideIn(options.useFxForSlide);
		//else if we're announcing...
		else if(options.announce) {
			//wait until all the items are added
			this.bar.iconMenu.addEvent('onItemsAdded', function(){
				//if we're not sliding in, then just pop open
				if(!options.useFxForSlide)this.bar.slideIn(false);
				//announce!
				this.bar.announce(options.announce);
				//this might break in Mootools 1.2
				//but there's no built in method in 1.11 to
				//removeEvents for a class
				this.bar.iconMenu.$events['onItemsAdded']=[];
			}.bind(this))
		}
	},
	add: function(ids, getOffset, getHowMany, marqueeOptions){
		this.rtss.add(ids, {
			startAt: getOffset||0,
			maxHits: getHowMany||0
		});
		if(marqueeOptions) this.bar.marqueeFx.announce(marqueeOptions);
	},
	remove: function(ids, getOffset, getHowMany, marqueeOptions){
		this.rtss.remove(ids, {
			startAt: getOffset||0,
			maxHits: getHowMany||0
		});
		if(marqueeOptions) this.bar.marqueeFx.announce(marqueeOptions);
	},
	toggleLocation: function(options){
		options = $merge({
			position: (this.position=='bottom')?'inpage':'bottom',
			hidden: true,
			useFxForSlide: true
		}, options);
		//set up the data
		options.data = options.data || (this.rtss.data && this.rtss.data.products)?this.rtss.data.products:[];
		//set up event to create new bar when the current one is hidden
		this.bar.addEvent('onSlideOut', function(){
			//create the bar
			this.createBar(options);
			//update the message
			this.inBarMessage();
			//slide in the bar
			this.bar.slideIn(options.useFxForSlide);
			//set up method to fire when it's done animating;
			//fire an event for onAffix/onEmbed
			//update the user prefs
			var onComplete = function(){
				this.fireEvent(options.newLocation == 'bottom'?'onAffix':'onEmbed');
				this.prefs.set('docPosition', options.position);
			};
			//if we're using effects
			if(options.useFxForSlide) {
				//slide everything in
				this.bar.toolbarSliderContainer.smoothShow({
					duration: this.bar.options.slideInFxOptions.duration / 2
					//and when we're done, execute the complete method
				}).chain(onComplete.bind(this));
			//else we're not doing anything fancy, just pop it open
			} else {
				this.bar.toolbarSliderContainer.show();
				onComplete.apply(this);
			}
			//update the current position
			this.position = options.position;
		}.bind(this));
		//go past the normal slideOut position so the bar disappears entirely
		if(this.position == "bottom") this.bar.options.slideOutPosition += -20;
		else this.bar.options.slideOutPosition = 0;
		//slide out (which, when complete, will fire onSlideOut and our function above)
		this.bar.slideOut();
	},
	affix: function(options) {
		this.toggleLocation($merge({
			position: 'inpage'
		}, options));
	},
	embed: function(options) {
		this.toggleLocation($merge({
			position: 'bottom'
		}, options));
	},
	createRTSSManager: function(){
		this.rtss = new RTSS.History.JsonP(this.options.rtssOptions);
		//set up event handling
		this.rtss.addEvent('onSuccess', function(data){
			this.loadHistory(data);
			this.inBarMessage();
						this.bar.slideIn();
				}.bind(this));
				this.rtss.addEvent('onFailure', function(data){
						this.inBarMessage();
				}.bind(this));
				function announce(ids, event) {
			if(ids.length) {
				if($type(ids) != "array") ids = [ids];
				try {
					var name = this.getItemById(ids.getLast()).txt;
					if(name){
						switch(event) {
							case "add":
								this.bar.marqueeFx.announce({message: "Added " + name + " to your history."});
								return;
							case "remove":
								this.bar.marqueeFx.announce({message: "Removed item from your history."});
								return;
						}
					}
				} catch(e){dbug.log(e);}
			}
			if(event == "empty") this.bar.marqueeFx.announce({message: "Your history is now empty"});
		}
		this.rtss.addEvent('onAdd', function(ids){
			announce.apply(this, [ids, "add"]);
		}.bind(this));
		this.rtss.addEvent('onRemove', function(ids){
			if(this.rtss.data.totalProducts > 0) announce.apply(this, [ids, "remove"]);
			else announce.apply(this, [[], "empty"]);
		}.bind(this));
	},
	getItemById: function(id){
		var item = this.rtss.data.products.filter(function(product){
			return product.id == id;
		});
		if(item.length) return item[0];
		else return false;
	},
	getPrefs: function(){
		this.prefs = new Hash.Cookie(this.options.cookieName, this.options.cookieOptions);
        this.prefs.set('docPosition',this.options.defaultPosition);
        this.prefs.set('visible',this.options.defaultVisible);
		this.prefs.save();
	},
	verifySessionCookie: function(){
		if(!Cookie.get('XCLGFbrowser')) {
			new Jlogger({
				tag: 'missing-sess-cookie',
				fireOnce: true
			}).ping();
			// We don't have a session cookie, load or create the cache cookie
			// so we can add this productId to the list of products to be added later
			var cacheCookie = new Hash.Cookie(this.options.cacheCookieName, this.options.cookieOptions);
			if(cacheCookie.length > 0 && cacheCookie.get('productCache').split(',')[0].length > 0) {
				var productCache = cacheCookie.get('productCache').split(',');
				// Ok, we are already storing some products, add the new one
				if(PageVars.get('assetId', 'number') && !productCache.contains(PageVars.get('assetId', 'string')))
					productCache.push(PageVars.get('assetId', 'number'));
				cacheCookie.set('productCache', productCache.join(','));
			} else {
				// This is the first product in the cache, create the array
				// to hold the product cache and set it in the Hash
				if(PageVars.get('assetId')) cacheCookie.set('productCache', PageVars.get('assetId', 'number'));
			}
		} else {
			this.options.defaultVisible = true;
		}
	},
	loadHistory: function(data){
		data = data || this.rtss.data;
		//need to handle corruption here!
		//check if the history has been altered and reload if needed
		if(data.products.length) {
			this.bar.addData(data.products);
			this.fireEvent('onDataLoad');
		}
	},
	getHistory: function(offset, howMany) {
		this.rtss.get(offset||0, $pick(howMany, this.options.rtssOptions.defaultRequestParams.maxHits));
	},
	loadNeededItems: function(){
		//get the current range
		var range = this.bar.iconMenu.calculateRange();
		//{start: index, end: range.length+index, elements: range}
	
		//if the iconMenu contains all the items that there are, then exit
		if(this.bar.iconMenu.imgs.length == this.rtss.data.totalProducts) {
			//dbug.log('we\'ve got everything already; imgs %s == total products %s', this.bar.iconMenu.imgs.length, this.rtss.data.totalProducts);
			return;
	
		//else if the end of the range is less than the current number of icons we have minus
		//a full page more of icons, then we don't need to load anything, so exit.
		} else if (range.end < this.bar.iconMenu.imgs.length-range.elements.length) {
			//dbug.log('range.end %s < imgs %s - range.length %s', range.end, this.bar.iconMenu.imgs.length, range.elements.length);
			//dbug.log('we\'re not near the end');
			return;
	
		//else we need more icons
		} else {
			//dbug.log('getting next set offset %s: howmany %s', this.bar.iconMenu.imgs.length, this.options.itemsPerSet);
			//dbug.log('range end %s, total products: %s', range.end, this.rtss.data.totalProducts);
			this.rtss.getMore();
		}
	},
	inBarMessage: function(){
		var totalProds = (this.rtss.data) ? $pick(this.rtss.data.totalProducts, 0) : 0;
	
		/* UPDATE LINKS */
		if (!totalProds) {
			$('removeSelected').hide();
			$('clearall').hide();
		} else {
			$('removeSelected').setStyle('display','');
			$('clearall').setStyle('display','');
		}
	
		/* UPDATE COMPARE */
		if (totalProds < 2) {
			var noCompare = ($('noCompare')) ? $('noCompare') : new Element('a', {
				id: 'noCompare',
				alt: 'You need to have at least two products here to compare them'
			}).set('text','Compare').inject($('compareLink','after'));
			$('compareLink').hide();
			noCompare.setStyle('display','');
		} else {
			if ($('noCompare')) $('noCompare').hide();
			$('compareLink').setStyle('display','');
		}
	
		/* UPDATE MESSAGE */
		switch(totalProds) {
			case 0:
				// Purposely let this case fall through to case 1 for the messaging
				$('loadingImage').effect('opacity').start(0);
			case 1:
				$('inBarMsg').set('html','When you look at products on CNET, we\'ll keep track of them here.').show();
				break;
			case 2:
				$('inBarMsg').set('html','You can create side-by-side comparisons of the products that interest you most.').show();
				break;
			case 3:
			case 4:
			case 5:
				if (this.position == 'bottom')
					$('inBarMsg').set('html','You can create side-by-side comparisons of the products that interest you most.').show();
				else
					$('inBarMsg').hide();
				break;
			default:
				$('inBarMsg').hide();
		}
	}
});/*	Script: RTSS.History.js
		Handles user history data from RTSS for CNET commerce toolbar.

		Author:
		Aaron Newton

		Dependencies:
		Mootools & CNET - <RTSS.js> and all its dependencies
		Optional - <jsonp.js>

		Class: RTSS.History
		Handles user history data from RTSS for CNET commerce toolbar.

		Arguments:
		options - (object) a key/value map of options

		Options:
		All options defined in <RTSS>, plus

		defaultRequestParams - (object) an object of key/value pairs to be included in the query string of all requests; see below for defaults.

		Default Request Parameters:
		_sideid_ - default value is 3
		_editionid_ - default value is 3
		startAt - defaults to zero
		maxHits - defaults to zero

		Instance Variables:
		operation - the operation being performed (add, get, remove)

		Events:
		onAdd - (function) callback executed when items are added; passed the array of ids added
		onRemove - (function) callback executed when items are removed; passed the array of ids added
		onClear - (function) callback executed when items are removed
		onGetHistory - (function) callback executed when items are removed
	*/
RTSS.HistoryBase = {
	options: {
		defaultRequestParams: {
			_siteid_: 3,
			_editionid_: 3,
			startAt: 0,
			maxHits: 0
		}
	},
/*	Property: add
		Adds one or more items to a user's history.

		Arguments:
		ids - an array of ids or a single id; all integers
		options - a key/value set of options passed along to request as additional query string key/values
 */
	add: function(ids, options){
        this.operation = 'add';
        var remote = this.request($merge({
			cmd: 'addProducts',
			productIds: ($type(ids) == "array")?ids:[ids]
		}, options));
		remote.addEvent('onComplete', function(){
			this.fireEvent('onAdd', ids);
		}.bind(this));
		return remote;
	},
/*	Property: remove
		Removes one or more items from a user's history.

		Arguments:
		ids - an array of ids or a single id; all integers
		options - a key/value set of options passed along to request as additional query string key/values
 */
	remove: function(ids, options){
        this.operation = 'remove';
        var remote = this.request($merge({
			cmd: 'removeProducts',
			productIds: ($type(ids) == "array")?ids:[ids]
		}, options));
		this.data.products = this.data.products.filter(function(product){
			!ids.contains(product.id)
		});
		remote.addEvent('onComplete', function(){
			this.fireEvent('onRemove', ids);
		}.bind(this));
		return remote;
	},
/*	Property: empty
		Empties a user's history entirely.
 */
	empty: function() {
        this.operation = 'empty';
        var remote = this.request($merge({
			cmd: 'clearHistory'
		}));
		remote.addEvent('onComplete', function(){
			this.fireEvent('onClear');
		}.bind(this));
		return remote;
	},
/*	Property: get
		Retrieves a user's history items

		Arguments:
		startAt - (integer) the offset of where to start
		maxHits - (integer) the number of items to retrieve; defaults to the maxHits option value
 */
	get: function(startAt, maxHits){
        this.operation = 'get';
        var remote = this.request({
				cmd: 'retrieveHistory',
				startAt: startAt,
				maxHits: $pick(maxHits, this.options.defaultRequestParams.maxHits)
		});
		remote.addEvent('onComplete', function(){
			this.fireEvent('onGetHistory');
		}.bind(this));
		return remote;
	},
/*	Section: Private Methods

		Property: request
		Internal; sends a request for all history transations.

		Arguments:
		data - a set of key/value url parameters
 */
	request: function(data){
        //:TODO: Refactor this stuff
        //If there is a userHistoryCache cookie, we need to take that data into account
        if(Cookie.get('userHistoryCache')) {
            var cacheCookie = new Hash.Cookie('userHistoryCache', {path: '/'});
            //First, find out if we have the sessionCookie yet
            if(Cookie.get('XCLGFbrowser')) {
                //Ok, we have a session, so make sure to add the cookie products to RTSS as well
                var ids = data.productIds||[];
                var cookieProducts = cacheCookie.get('productCache').split(',');
                // Loop through them and add them to the existing array
                cookieProducts.each(function(prod){
                    ids = ($type(ids) == 'array')?ids:[ids];
                    ids.merge([prod]);
                });
                data.productIds = ids;
                // Now that we are all caught up, remove the cookie
                Cookie.remove('userHistoryCache', {path: "/"});
                //Send off the request to RTSS with the new set of productIds
                return this.parent(
                    this.makeUrl(
                        Hash.toQueryString($merge(this.options.defaultRequestParams, data)).replace('%25', ',', 'g')
                    )
                );
            } else {
                //Initialize the object that will mimic the data sent to handleResponse
                var cookieData = {
                    offset: 0,
                    products: [],
                    totalProducts: 0,
                    version: 0
                };
                //Perform logic on the cookie data depending on the operation
                switch(this.operation) {
                    case 'add':
                    case 'get':
                        //Pull out the productIds in the cookie
                        var cookieProducts = cacheCookie.get('productCache').split(',');
                        //Set the totalProducts count on the cookieData object
                        cookieData.totalProducts = cookieProducts.length;
                        //Loop through and fetch the api data for each productId and
                        //use it to populate the products array in the cookieData object
                        new CNETAPI.Utils.TechProduct({
                            applicationName: 'userHistory',
                            onSuccess: function(results){
                                dbug.log(results);
                                try {
                                    cookieData.products = results.map(function(product){
                                    return {
                                        productImage: product.data.ImageURL[0].$,
                                        productLink: product.data.ReviewURL,
                                        productName: product.data.Name,
                                        productId: product.data.id
                                    }
                                });
                                } catch (e) {
                                    dbug.log(e);
                                }
                                dbug.log("Calling handleResponse with cookieData: ", cookieData);
                                this.handleResponse(cookieData);
                            }.bind(this)
                        }).getMany(cookieProducts);
                        break;
                    case 'remove':
                        //Pull out the productIds in the cookie
                        var cookieProducts = cacheCookie.get('productCache').split(',');
                        //Remove ids from the cookie that match any of the ids passed to the remove function
                        data.productIds.each(function(prod){
                            if(cookieProducts.contains(prod)) cookieProducts.remove(prod);
                        });
                        //Now we need to conver the array back into a string to store back in the cookie
                        var prodString = cookieProducts.join(',');
                        //Now set the product list back in the cookie (this should auto-save)
                        cacheCookie.set('productCache', prodString);
                        cacheCookie.save();
                        break;
                    case 'empty':
                        //Just clear the productCache in the cookie
                        Cookie.remove('userHistoryCache', {path: '/'});
                        break;
                }
            }
        } else {
            return this.parent(
                this.makeUrl(
                    Hash.toQueryString($merge(this.options.defaultRequestParams, data)).replace('%25', ',', 'g')
                )
            );
        }
    },
/*	Property: handleResponse
		Internal; handles the response from the server

		Arguments:
		data - (mixed) the data returned; expects an object
 */
	handleResponse: function(data){
		try {
            //check the response and parse it if necessary
            if($type(data) == "string") {
                var parsedData = JSON.evaluate(data, true);
                if (!data || !data.products) this.handleError(data);
                else data = parsedData;
            }
            if(data) {
                //get all the item ids currently in the data store
                var ids = [];
                if(this.data) {
                    ids = this.data.products.map(function(item){
                        return item.id;
                    });
                } else {
                    //this.data is not yet set, so just set it now
                    this.data = $merge(data);
                    //careful, that $merge request may have altered the product set
                    this.data.products = [];
                }
                //store the (possibly new) total products value
                this.data.totalProducts = data.totalProducts;
                var products = data.products.map(function(item){
                    return {
                        img: item.productImage.replaceAll("'", ""),
                        url: item.productLink,
                        txt: item.productName,
                        id: item.productId
                    };
                }).filter(function(item){
                    return !ids.contains(item.id);
                });
                data.products = products;
                dbug.log('products: ', products);
                this.data.products.extend(data.products);
                this.parent(data);
            }
        } catch(e){
			dbug.log('error handling history data: ', data, e);
            this.handleError(data);
        }
	},
	getMore: function(maxHits){
        return this.get((!this.data || !this.data.products)?0:this.data.products.length,
						 $pick(maxHits, this.options.defaultRequestParams.maxHits));
	}
};
RTSS.History = RTSS.extend(RTSS.HistoryBase);
/*	Class: RTSS.History.JsonP
		Extends <RTSS.JsonP> functionality into <RTSS.History>
	*/
if(RTSS.JsonP) RTSS.History.JsonP = RTSS.JsonP.extend(RTSS.HistoryBase);
/* do not edit below this line */
/* Section: Change Log

$Source: /cvs/main/flatfile/html/rb/js/commerce/implementations/historybar/RTSS.History.js,v $
$Log: RTSS.History.js,v $
Revision 1.8  2008/02/06 23:28:33  kruegerw
Making changes to adopt the 'b' version of the toolbox

Revision 1.6  2007/11/19 21:26:22  kruegerw
Making fixes for the recent products toolbox

Revision 1.5  2007/09/21 21:37:32  kruegerw
Checking in changes for keeping the box collapsed when there is no history and adding some Jlogger stuff

Revision 1.4  2007/08/31 22:25:06  kruegerw
Changing around the userHistoryCache logic a bit

Revision 1.3  2007/08/23 18:20:08  newtona
updated cat files: added assets back to rb.global; added stickywin to commerce.global (for now; used for the moment in history bar)
added docs to element.position
removed .reverse in RTSS.History

Revision 1.2  2007/08/21 00:58:07  newtona
RTSS.History, RTSS.JsonP: added events for add, remove, empty, etc.
RTSS.js: most methods now return the remote class (XHR or JsonP)
UserHistory: add logic to announce actions (add, remove, etc.)
ProductToolbar: implemented place-holder compare function; removed some dbug lines
JsonP: tweaking retry logic

Revision 1.1  2007/08/20 21:20:42  newtona
first big check in for RTSS History


*/
/*	
Script: Jlogger.js
	Enables clientside logging for any client side data or events.
	
License:
	No License; internal CNET code not intended for reuse.
	*/
var Jlogger = new Class({
	Implements: [Options, Events],
	options: {
/*	ctype: false,
		cval: false,
		tag: false,
		element: false,
		event: false,
		useraction: false,
		fireOnce: false,
		onPing: $empty */
	},
	errors: 0,
	fired: false,
	active: true,
	initialize: function(options) {
		var defaults = (typeof PageVars != "undefined")?{
			ontid: PageVars.get('nodeId', 'number'),
			siteId: PageVars.get('siteId', 'number'),
            asId: PageVars.get('assetId', 'number'),
			ptId: PageVars.get('pageType', 'number'),
			edId: PageVars.get('editionId', 'number')
		}:{};
		this.setOptions(defaults, options);
		if(this.options.element == 'window') this.options.element = window;
		this.setup();
	}, 
	setup: function(){
		if(!$(this.options.element)) return;
		var opt = this.options; //saving bytes
		if ($type(opt.tag) && $type(opt.element) && $type(opt.event)){
			//else tag is set, element is set, and event is set, log this info and...
			dbug.log('event observe(element: '+opt.element+', event: '+opt.event+', tag: '+opt.tag+')');
			//if the event == "load" and the observed element is the window, execute the ping immediately
			if(opt.event == 'load' && opt.element == window) opt.executeNow = true;
			//observe the elemnt for the event.
			if(opt.element != window) $(opt.element).addEvent(opt.event, this.ping.bind(this));
			else if(opt.event != 'load') $(opt.element).addEvent(opt.event, this.ping.bind(this));
		}
	},
	//generates the url to ping DW and returns it
	makeURL: function(tag) {
		var url = 'http://dw.com.com/redir?';
		var opt = this.options;//saving bytes
		if($type(opt.ontid)) url+= 'ontid='+opt.ontid+'&';
		if($type(opt.siteId)) url+= 'siteid='+opt.siteId+'&';
				if($type(opt.asId)) url+= 'asId='+opt.asId+'&';
				if($type(opt.ptId)) url+= 'ptId='+opt.ptId+'&';
		if($type(opt.edId)) url+= 'edId='+opt.edId+'&';
		if($type(opt.ctype)) url+= 'ctype='+opt.ctype+'&';
		if($type(opt.cval)) url+= 'cval='+opt.cval+'&';
		if($type(opt.useraction)) url+= 'useraction='+opt.useraction+'&';
		url+= 'tag='+opt.tag+'&destUrl=/i/b.gif';
		//append a date value so that the browser doesn't cache the request
		url+= '&uniquePingId='+new Date().getTime();
		return url;
	},
	ping: function(url, force) {
		//if fireOnce is set and this hasn't yet fired, or fireOnce isn't set or is false, and this observer is active
		//then ping dw
		if (force || (((!this.fired && this.options.fireOnce) || !this.options.fireOnce) && this.active)) {
			//if the url isn't passed in to this function, get it from the makeURL function in this class
						url = ($type(url) != 'string' || url.length == 0)?this.makeURL():url;
						//if the doc is loaded, continue with the ping (doing this before the doc is loaded will break IE)
			window.addEvent('domready', function(){
				new Element('img', {src: url});
				//just creating the img and setting its src will cause the browser to hit the url, you don't
				//need to append it to the DOM
				this.fired = true;
				dbug.log(this.options.tag + ': '+(this.options.event||'')+'\nping: '+url);
				this.fireEvent('onPing');
			}.bind(this));
		}
		return this;
	},
	pingTag: function(tag, force){
		return this.ping(this.makeURL(tag), force);
	},
	stopObserving: function(){
		//turns off this logger
		this.active = false;
		return this;
	},
	startObserving: function(){
		//turns it back on
		this.active = true;
		return this;
	}
});
/*	
Script: JlScroller.js
	Extends the jLogger class to capture scroll events.
	
License:
	No License; internal CNET code not intended for reuse.
	*/
var JlScroller = new Class({
	Extends: Jlogger,
	setup: function() {
		//if passed in: tag, event, and event == "scrollTo", set up a scroller monitor
 		if (this.options.tag && this.options.event && this.options.event == "scrollTo") {
			//log this if debug=true is in the url
			dbug.log('event observe(element: '+this.options.element+', event: '+this.options.event+', tag: '+this.options.tag+', scrollTo: '+this.options.scrollTo+')');
			//set up scrollTo monitor
			this.setUpScrollTo();
		} else this.parent();
	},
	//if we're observing a scroll event, this function handles the setup.
	setUpScrollTo: function() {
		//start with top & bottom set to invalid values (i.e. numbers that the browser can't create on the page)
		var top = -1;
		var bottom = -1;
		try {
			var scrollToTop = this.options.scrollTo.top; //saving bytes
			//if scrollTo.top is a number, set that number to the top range
			if ($chk(scrollToTop) && $chk(scrollToTop.toInt)) top = scrollToTop.toInt();
			//else top is not a number, so set top to the top of the element passed in
			else if ($(scrollToTop)) top = $(scrollToTop).getTop();
			//save this top value. we're going to need this variable even if it wasn't passed in
			//if all that was passed in was bottom: <elementId>, we'll need to get the top of that
			//element to figure out the bottom of it.
			tmpTop = top;
			//of the bottom is set
			var scrollToBtm = this.options.scrollTo.bottom; //saving bytes
			if($chk(scrollToBtm)) {
				//if top is still -1 and bottom isn't a number (it's assumed then that it's an element id)
				if (top == -1 && $(scrollToBtm))
					//and save its top
					top = $(scrollToBtm).getTop();
				//if the bottom is the same as the top (in the case of passing in the same element id for both)
				//or the top wasn't set, then set the bottom of the range to the bottom of the element
				//which is calculated by adding the top position of the element to the height of the element
				if (scrollToBtm == scrollToTop || !$chk(this.options.scrollTo.top))
					if ($(scrollToBtm)) bottom = $(scrollToBtm).getStyle('height').toInt() + top;
				else if ($chk(parseInt(scrollToBtm)) && tmpTop >= 0) //else, if the bottom is a number and top is set to something
					bottom = tmpTop + scrollToBtm; //add it to the top value
				else if ($chk(parseInt(scrollToBtm))) //else the top isn't set, so the bottom is just the number passed in
					bottom = scrollToBtm;
				else if ($(scrollToBtm))//else the bottom is an element, so the bottom is equal to the top of it.
					bottom = $(scrollToBtm).getTop();
			} else { //else, bottom isn't set at all, so set it's numerical location = to the top
				bottom = top;
			}
			//set top back to our placeholder
			top = tmpTop;
			//if top is still invalid (i.e. -1), make it equal to the bottom value
			if (top < 0) top = bottom;
			dbug.log("new tripwire (%s): top: %s, bottom: %s", this.options.tag, top, bottom);
			if (top >= 0 && bottom >=0) {
				//if the top & the bottom are both valid numbers (>0), set up the scroll observer
				window.addEvent('scroll', this.isOnScreen.bind(this, [top, bottom]));
				//check to see if this range is on screen right NOW
				this.isOnScreen(top, bottom);
			}
		} catch(e) {
			//if there was an error, the DOM might not be ready yet, let's wait a moment and try again
			//let's cap these retries at 10 times.
			if (this.errors < 10) {
				dbug.log('JlScroller error: %o, attempt #: %s', e, this.errors);
				this.errors++;
				this.setUpScrollTo.delay(20);
			} else dbug.log('giving up attempt to set up instance of JlScroller for %s', this.options.tag);
		}
	},
	isOnScreen: function(top, bottom) {
		//is the area defined on the screen
		// top: the top of the area
		// bottom: optional - the bottom of the zone
		// if bottom is not defined, then the area is just a line = top
		var dim = this.getScreenDimensions(); //get the dimensions of the browser screen
		var scroll = this.getScrollOffset(); //get the scroll offset
		try {
			//if the top of our range is between the scroll offset and the scroll offset + the window height (i.e. is visible)
			//or if the bottom is in that range
			if ((top > scroll.y && top < scroll.y+dim.h) || (bottom > scroll.y && bottom < scroll.y+dim.h)) {
				//if this hasn't been marked as having fired...
				//ping dw; note that ping sets this.fired to true
				if(!this.fired) this.ping();

			//if the user scrolled and the range is NOT visible, AND fireOnce is not set to true
			//set fired back to false (so it can fire again, but only once for each time that the
			//range becomes visible)
			} else if (this.fired && !this.options.fireOnce) this.fired = false;
		} catch(e) { dbug.log("isOnScreen error: %s", e) }
	},
	getScreenDimensions: function() {
		return {w: window.getWidth(), h: window.getHeight()};
	},
	getScrollOffset: function() {
		return {x: window.getScrollLeft(), y: window.getScrollTop()};
	}
});
/*	Script: ProductToolbar.js
		Builds a toolbar of icons for each product in the list; used for CNET Redball user history toolbar.

		Author:
		Aaron Newton

		Dependencies:
		Mootools 1.11 - <Core.js>, <Class.js>, <Class.Extras.js>, <Array.js>, <Element.js>, <String.js>, <Function.js>, <Number.js>, <Element.Event.js>, <Element.Selectors.js>, <Element.Dimensions.js>, <Fx.Base.js>, <Fx.Css.js>, <Fx.Style.js>, <Fx.Styles.js>, <Fx.Elements.js>, <Fx.Slide.js>, <Ajax.js>, <XHR.js>, <Cookie.js>, <Json.js>, <Color.js>
		CNET - <element.pin.js>, <element.dimensions.js>, <element.shortcuts.js>, <Fx.SmoothMove.js>, <IframeShim.js>, <Fx.Marquee.js>, <StickyWin.js>, <StickyWin.Modal.js>, <modalizer.js>, <jlogger.js>, <jlscroller.js>, <jsonp.js>

		Class: ProductToolbar
		Builds a toolbar of icons for each product in the list; used for CNET Redball user history toolbar.

		Arguments:
		options - (object) a key/value map of options

		Options:
		compareLinks - (DOM element) DOM reference or id for the link that initiates a compare event
		container - (DOM element) DOM reference or id for the container of the iconMenu
		marquee - (DOM element) DOM reference or id for the container element passed to <Fx.Marquee>
		marqueeFxOptions - (object) options passed to <Fx.Marquee>
		toolbarSliderContainer - (DOM element) DOM reference or id for the containing element of the entire toolbar
		toolbarSliderContainerId - (string) the id of the container element for the entire bar
		data - (array) array of items to add to the toolbar at instantiation
		iconMenuOptions - (object) options passed to <IconMenu>
		subTitle - (DOM element) DOM reference or id for the title div that is shown when the user mouses over an icon
		hideLinks - (string; css selector(s)) selector to get dom elements that will hide/unhide the history bar
		hidden - (boolean) whether or not the bar is set to display:none on intantiation; defaults to false
		pin - (boolean) whether or not the bar is in a fixed location and doesn't move when the window is scrolled; defaults to true
		slideInFxOptions - (object) Fx options passed along to <Fx.Style> for the open/close effect
		slideInPosition - (integer) the "visible" value for the slide Fx
		slideOutPosition - (integer) the "closed" value for the slide Fx
		slideInPositionIE - (integer) the "visible" value for the slide Fx *for IE 6*
		slideOutPositionIE - (integer) the "closed" value for the slide Fx *for IE 6*
		injectTarget - (DOM element or id) where to inject the bar; defaults to null; if value is false, the bar is not created and you must execute <createBody> yourself
		injectMethod - (string) injectInside, injectTop, injectBefore, or injectAfter; references the injectTarget element
		baseHref - (string) url location of all the art files
		innerHTML - (string) the html body of the toolbar
		helpLinks - (string) css selector for all the links in the toolbar that launch the help
		helpText - (string) the html of the help text

		Events:
		onSlideIn - (function) event fired when the bar slid open
		onSlideOut - (function) event fired when the bar slid closed
		onDestroy - (functon) event fired when a class is removed

		Instance Variables:
		toolbarSliderContainer - the container of the entire toolbar
		marquee - the dom element that contains marquee messages
		marqueeFx - <Fx.Marquee> instance for the marquee
		titleMarqueeFx - effect used to hide the marquee when other messaging needs the space
		container - the container of the <IconMenu>
		data - local data store for the items in the menu
		subTitle - the container for the hover text for icons
		subTitleFx - the effect for the subTitle to fade it in and out
		subTitleMover - the effect for moving the subTitle around
		subTitleShown - which subTitle is currently visible
		shim - an instance of <IframeShim> for the bar
		listContainer - the ul item that contains all the icons in the menu
		iconMenu - the instance of <IconMenu>
		slider - the effect for sliding the bar open and closed
	*/
var ProductToolbar = new Class({
	Implements: [Options,Events],
		options: {
		compareLinks: '#compareLink',
		container: 'userHistoryBar',
		marquee: 'marquee',
		marqueeFxOptions: {
			showEffect: {
				left: [0, 41],
				opacity: [0,1]
			}
		},
		toolbarSliderContainer: 'personalizer',
		toolbarSliderContainerId: 'personalizer',
		data: [],
		iconMenuOptions: {
			removeLinks: '#removeSelected',
			clearLinks: '#clearall',
			initialFocusDelay: 200,
			onFocusDelay: 100,
			onBlurDelay: 50
				},
		subTitle: 'itemName',
		hideLinks: '#hideHistory, #hideHistoryIcon, #marquee, #historyTitle, #hideHistoryX',
		onDestroy: $empty,
		onSlideIn: $empty,
		onSlideOut: $empty,
		slideInFxOptions: {
			transition: Fx.Transitions.Back.easeIn,
			duration: 1000
		},
		slideInPosition: -10,
		slideOutPosition: -89,
		slideInPositionIE: 0,
		slideOutPositionIE: 83,
		pin: true,
		injectTarget: null,
		// 1.2 syntax has changed, so injectBefore becomes inject(myelem,'before') Below doesn't solve the problem of inject method
  	injectMethod: 'inject',
		baseHref: 'http://i.i.com.com/cnwk.1d/html/historytest/art',
		innerHTML:
			'<div id="userStatus">\
				<div class="margins">\
					<div id="historyStatus">\
						<div id="marquee">\
							<span class="fxMarquee"><a id="historyTitle">RECENTLY VIEWED PRODUCTS</a></span>\
						</div>\
						<div id="itemName"></div>\
						<a id="hideHistory" title="Show your product history"></a>\
						<a class="whatsThis"><span id="whatsThis">What\'s this?</span></a>\
						<div id="optionLinks">\
						<img src="%baseHref%/remove_this_bar.png" width="80" height="8" alt="Remove this bar" class="remove"/>\
							</div>\
					</div>\
				</div>\
			</div>\
			<div id="history">\
				<div class="margins">\
					<div class="iconbar" id="userHistoryBar">\
						<a alt="scroll left" id="scrollLeft" style="opacity: 0;visibility: hidden"></a>\
						<div id="inBarMsg" style="display:none">When you look at products on CNET, we\'ll keep track of them here.</div>\
							<img src="%baseHref%/ajax-loader-inside_v2.gif" alt="loading" id="loadingImage" style="background:transparent url(http://i.i.com.com/cnwk.1d/html/historytest/art/zbord.gif) no-repeat scroll 0%;cursor:pointer;margin:0px;padding:0px;position:relative;z-index:7;width:264px;height:70px;"/>\
						<a alt="scroll left" id="scrollRight" style="opacity: 0;visibility: hidden"></a>\
					</div>\
					<div id="toolBoxOptions" class="clearfix">\
						<!--a id="showOnAll">Show on every page</a--> \
					</div>\
					<div id="actionLinks">\
						<a alt="Compare selected" id="compareLink">Compare</a>\
						<a href="" id="removeSelected" onclick="return false;">Remove selected</a> \
						<a href="" id="clearall" onclick="return false;">Clear all</a>\
					</div>\
				</div>\
			</div>\
			<div id="whatsThisBox"></div>',
			hidden: false,
			helpLinks: 'a.whatsThis, img.whatsThis',
			helpText:"<span id=\"wtbTitle\">Your recently viewed products</span><div><p>Scroll through a history of the products you've been looking at on CNET, without leaving this page! For side-by-side comparisons, select the products you are interested in and click the \"Compare\" button.</p></div>"
	},
		type: 'affixed',
		initialize: function(options){
		//set the options passed in
		this.setOptions(options);
				//setup any jloggers needed for the toolbox
				this.setupJloggers();
				//yay. ie6
		if(Browser.Engine.trident4) {
			this.options.slideInPosition = this.options.slideInPositionIE;
			this.options.slideOutPosition = this.options.slideOutPositionIE;
		}
		if(this.options.injectTarget || this.options.injectTarget == null) this.createBody();
		//the container for the whole toolbar - the part that slides in and out
		else this.toolbarSliderContainer = $(this.options.toolbarSliderContainer);
				//get the marquee - the area where action messages to the user will be displayed
		this.marquee = $(this.options.marquee);
		//the container where all the icons are held
		this.container = $(this.options.container);
		//place holders for data
		this.data = [];
		//load the data passed in
		this.buildSet(this.options.data);
		//create the effects and references to the title popup
		this.subTitle = $(this.options.subTitle);
		this.subTitleFx = this.subTitle.effect('opacity', {wait: false});
		this.subTitleMover = new Fx.SmoothMove(this.subTitle, {
			wait: false,
			transition: Fx.Transitions.Quad.easeInOut
		});
		//setup methods
		this.setupHideLinks();
		this.setupCompareLinks();
		this.setupMarqueeFx();
		this.setupHelpTxt();
				//iframe shim stuff
		if(Browser.Engine.trident4 || (Browser.Platform.mac && Browser.Engine.gecko)) this.makeShim();
		//pin
		//if(this.options.pin && this.shim) this.toolbarSliderContainer.pin();
	},
/*	Section: Public Methods

		Property: loadData
		Loads icons for the data passed in. Does NOT create icons for this data (see <buidSet> for this operation).

		Arguments:
		data - (object) a key/value set of data
		overwrite (boolean; optional) if true, overwrite the current data with the data passed in.

		Data:
		Looks like this:

(start code)
[
	{
		img: 'http://i.i.com.com/cnwk.1d/sc/32004258-2-60-0.gif',
		url:'http://...',
		txt: 'Nikon D80'
	},
	{
		img: 'http://i.i.com.com/cnwk.1d/sc/32069607-2-60-0.gif',
		url:'http://...',
		txt: 'Canon PowerShot SD800 IS'
	}
];
(end)
	*/
	loadData: function(data, overwrite) {
		//if overwriting
		if(overwrite) {
			//empty the icon menu
			//pass true to suppress the onEmpty event so we don't send
			//data to RTSS in <UserHistory>
			this.iconMenu.empty(true);
			//reset the data
			this.data = data;
		} else {
			//de-dupe the is passed in to the data already present
			var ids = this.data.map(function(item){return item.id});
			data.each(function(item){
				//push the new items into the data set
				if(!ids.contains(item.id)) this.data.push(item);
			}, this);
		}
	},
/*	Property: getItemByImgId
		Get the associated data item that matches the id of an image icon.

		Arguments:
		id - (string) the DOM id of the image.
	*/
	getItemByImgId: function(id) {
		var val;
		//get the index of the item
		this.data.each(function(data, index) {
			if(data.id == id) val = index;
		});
		//return the data
		return {index: val, data: this.data[val]};
	},

/*	Property: buildSet
		Builds an icon menu (appending to the current one if called more than once) .

		Arguments:
		data - (object) the data to load into the set (see <ProductToolbar.loadData>)
		from - (integer) you can load a subset of the data array by specifying an offset
		to - (integer) you can load a subset of the data array by specifying an endpoint
	*/
	buildSet: function(data, from, to){
		data = data || this.data;
		if(!$chk(from)) from = 0;
		if(!$chk(to) || to >= data.length) to = data.length;
				//.copy is depricated in 1.2. Not sure why
				//		var set = data.copy(from, to-from);
				var set = [];
				for (var x= (from || 0); x <= (to || data.length); x++)
						set.push(data[x]);

				this.loadData(set);
		this.buildIconMenu(set);
				this.scrolled = false;
				this.iconMenu.addEvent('onScroll', function(){
						this.scrolled = true;
				});
				//iconMenu.imgs
	},
/*	Property: addData
		Adds data to the toolbar.

		Arguments:
		data - (object) a collection of product data.
	*/
	addData: function(data){
		this.loadData(data);
		this.appendItems(data);
	},
	//internal; state holder
	subTitleShown: false,
/*	Property: showSubTitle
		Internal; shows the name on hover.

		Arguments:
		img - the img the user mouses over
 */
	showSubTitle: function(img){
		this.subTitleShown = img;
		this.subTitleFx.start(1);
		var range = this.iconMenu.calculateRange();
		var index = this.iconMenu.imgs.indexOf(img);
		//if the index of this icon is greater than .6 times the number of icons
		//in other words, farther right than just right of midpoint
		//put the name to the left of the icon
		var side = (index>((range.end-range.start)*.6)+range.start)?'Right':'Left';
		//dbug.log('index(%s) > end(%s)-start(%s)*.6 (%s): %s', index, range.end, range.start, ((range.end-range.start)*.6)+range.start, side);
		this.subTitle.empty().adopt(new Element('span', {
			'class':'subTitle',
			'styles':{
				position: 'relative'
			}
		}).adopt(new Element('b', {
						'html': this.getItemByImgId(img.id).data.txt
				}).adopt(
			new Element('img', {
				src: this.options.baseHref + '/tip_divot_y.gif',
				'class': 'tipDivot'+side
			})
		)));
		(function(){
			this.subTitleMover.start({
				relativeTo: img,
				position: "center",
				offset: {
					y: 2-window.getHeight(),
					x: (side == "Left")?-12:12
				},
				edge: "top"+side
			});
		}).delay(100, this);
	},
/*	Property: hideSubTitle
		Hides the hover name; see <showSubTitle>.

		Arguments:
		none;
 */
	hideSubTitle: function(){
		this.subTitleFx.start(0);
	},
/*	Property: setupHideLinks
		Internal; sets up links to hide the bar
 */
	setupHideLinks: function(){
		//for each link
				$$(this.options.hideLinks).each(function(el){
						//add a click event to toggle the bar open and closed
			el.addEvent('click', this.toggle.bind(this));
		}, this);
	},
/*	Property: setupCompareLinks
		Internal; sets up links to compare selected items
 */
	setupCompareLinks: function(){
		$$(this.options.compareLinks).each(function(el){
			//add a click event to compare selected items
			el.addEvent('click', this.compareSelected.bind(this));
		}, this);
	},
		setupJloggers: function(){
				// Ping DW when

				// Ping DW when a person expands the toolbox
				this.slideInLogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;expand',
						tag: 'box-expand',
						fireOnce: true,
						onPing: function(){
								PageVars.history.generalInteractionJlogger.ping();
						}
				});

				// Ping DW when a person minimizes the toolobx
				this.slideOutLogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;minimize',
						tag: 'box-minimize',
						fireOnce: true,
						onPing: function(){
								PageVars.history.generalInteractionJlogger.ping();
						}
				});

				// Ping DW when a user hovers over the product images
				this.prodHoverLogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;hover',
						tag: 'box-prod-hover',
						fireOnce: true,
						onPing: function(){
								PageVars.history.generalInteractionJlogger.ping();
						}
				});

				// Ping DW when a user hovers over "What's this?"
				this.helpJlogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;help',
						tag: 'box-help-hover',
						fireOnce: true,
						onPing: function(){
								PageVars.history.generalInteractionJlogger.ping();
						}
				});

				// Ping DW when there is one product in a user's history
				this.impZeroJlogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;imp0',
						tag: 'box-imp0',
						fireOnce: true
				});

				// Ping DW when there is one product in a user's history
				this.impOneJlogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;imp1',
						tag: 'box-imp1',
						fireOnce: true
				});

				// Ping DW when there are 2 or more products in a user's history
				this.impTwoJlogger = new Jlogger({
						ctype: 'tooltype;action',
						cval: 'box;imp2',
						tag: 'box-imp2',
						fireOnce: true
				});
		},
/*	Property: compareSelected
		Internal; compares selected items from the icon menu.
 */
	compareSelected: function(){
		var filter = function(img){
			return img.getProperty('id').toInt();
		};
		var ids = this.iconMenu.selected.map(filter);
		if(ids.length < 2 && this.iconMenu.imgs.length <= 4) ids = this.iconMenu.imgs.map(filter);
		if(ids.length < 2) alert("Use the checkboxes under the images to select at least two products, then click 'Compare'");
			else {
				location.href = '/4504-4_7-0.html?id='+ids.join('&id=')+'&tag=boxcoco';
						PageVars.history.generalInteractionJlogger.ping();
				}
	},
/*	Property: slideIn
		Slide the toolbar into its open state.
	*/
	slideIn: function(useFx){
		useFx = $pick(useFx, true);
		//make the effect
		this.makeSliderFx(Fx.Transitions.Back.easeOut);
		//fire the slide in effect
		var method = ((this.shim && this.position == "bottom")||!useFx)?'set':'start';
		this.slider[method](this.options.slideInPosition).chain(function(){
			//fire the event that says we've opened the toolbar
			this.fireEvent('onSlideIn');
		}.bind(this));
		if(method == 'set') this.fireEvent('onSlideIn');
	},
/*	Property: slideIn
		Slide the toolbar into its closed state.
	*/
	slideOut: function(useFx){
		useFx = $pick(useFx, true);
		//make the effect
		this.makeSliderFx(Fx.Transitions.Back.easeIn);
		//fire the slide out effect
		var method = ((this.shim && this.position == "bottom")||!useFx)?'set':'start';
		this.slider.clearChain();
		this.slider[method](this.options.slideOutPosition).chain(function(){
			//fire the event that says we've closed the toolbar
			this.fireEvent('onSlideOut');
		}.bind(this));
		if(method == 'set') this.fireEvent('onSlideOut');
	},
/*	Property: toggle
		Opens or closes the toolbar, depending on its current state.
	*/
	toggle: function(){
		//either slide in or out, depending on whether the bar is open
		if(!Browser.Engine.trident4 && this.toolbarSliderContainer.getStyle('bottom').toInt() <= this.options.slideOutPosition) {
			this.slideIn();
				} else if (Browser.Engine.trident4 && this.toolbarSliderContainer.getStyle('margin-top').toInt() == this.options.slideOutPosition) {
			this.slideIn();
				} else {
			this.slideOut();
		}
	},
/*	Property: announce
		Alerts the user to a particular icon by animating it.

		Arguments:
		options - EITHER an object of key/value pairs OR an integer; if you don't want to set any options and use the default, just pass in the integer

		Options:
		stayOpen - (boolean) leave the bar open after you announce? defaults to false
		delay - (integer) how long (in milliseconds) to leave the bar open before closing (if stayOpen is false);
			defaults to 5000
		highlight - (array of integer) indexs of the icons to highlight; defaults to [zero]
		resolveIcon - (boolean) slide the icon into place; defaults to true
		bounce - (boolean) bounce the icon after the resolve; defaults to true
	*/
	announce: function(options){
		//if the user passed in an integer, move the value into the options object
		if($type(options) == "number")	options = { highlight: [options] };
		if($type(options) == "array")	options = { highlight: options };
		//merge the defaults
		options = $merge({
			stayOpen: false,
			delay: 5000,
			highlight: [0],
			resolveIcon: true,
			bounce: false
		}, options);
		var resolveFx = {};
		//get the icons
		var icons = options.highlight.map(function(index, i){
			var icon = this.iconMenu.imgs[index];
			//get the parent of the icon
			var p = icon.getParent();
			resolveFx[i] = p.getComputedSize({mode: 'horizontal'});
			//set its width to zero if we're resolving it
			if(options.resolveIcon) p.setStyle('width',0);
			return p;
		}, this);
		//open the bar
		this.slideIn();

		//create the bounce function
		var bouncer = function(icons){
			//hey, if we aren't bouncing, then get out of here
			if(!options.bounce) return;
			var fx = new Fx.Elements(icons, {
				transition: Fx.Transitions.Back.easeOut,
				duration:200,
				fps: 40
			});
			var up = {};
			var down = {};
			icons.each(function(icon, i){
				up[i] = {top: 0};
				down[i] = {top: 0};
			});
			//set up the effect
			//up and down, up and down, weeee!
			fx.start(down).chain(function(){
				fx.start(up);
			}).chain(function(){
				fx.start(down);
			}).chain(function(){
				fx.start(up);
			});
			return;
		};
		//delay the resolution and bouncing
		(function(){
			//if we're resolving the icon
			if(options.resolveIcon){
				//open it up, then bounce
				new Fx.Elements(icons, {
					transition: Fx.Transitions.Back.easeOut,
					duration:700
				}).start(resolveFx).chain(function(){
					//set the width back to auto
					icons.each(function(icon, i){
						//if(!window.ie6) icon.setStyle('width', 'auto');
					});
					bouncer(icons);
				}.bind(this));
			//else just bounce
			} else bouncer();
		}).delay(500, this);
	},
/*	Property: destroy
		Destroys the instance of the class.
	*/
	destroy: function(){
				this.fireEvent('onDestroy');
				this.toolbarSliderContainer.remove();
	},
/*	Section: Private Methods

		Property: createBody
		Internal; creates the body using the html in the options
 */
	createBody: function(){
		var html = this.options.innerHTML.replace("%baseHref%", this.options.baseHref, 'g');
				if (Browser.Engine.trident4) html = html.replace('.png', '.gif', 'g');
		//create a new container for the the toolbar,
		this.toolbarSliderContainer = new Element('div', {
			id: this.options.toolbarSliderContainerId,
			styles: {
				display: this.options.hidden?'none':'block'
			}
		//set the html to that passed in; parse in the base href
		//then inject into the target using the specified method
		}).set('html',html)[this.options.injectMethod](this.options.injectTarget||document.body);
		//this is f'ed up, but IE needs this for some reason
		if(Browser.Engine.trident4) $$('#scrollRight, #scrollLeft').setStyle('width', 8);
				//Add a hover effects to the title bar if it is closed
				if($('userStatus')){
						$('userStatus').addEvent('mouseover', function(){
								this.addClass('hover');
						});
						$('userStatus').addEvent('mouseout', function(){
								this.removeClass('hover');
						});
				}
		},
/*	Property: makeShim
		Internal; creates an iframeShim for the toolbar
 */
	makeShim: function(){
		//make an iframeshim
		var opt = this.options;
		this.shim = new IframeShim({
			element: this.toolbarSliderContainer
		});
		//set up events to mirror it to the behavior
		this.addEvent('onSlideIn', function(){
				this.shim.position();
		});
		this.addEvent('onSlideOut', function(){
				this.shim.position();
		});
	},
/*	Property: setupMarqueeFx
		Internal; create a marquee effect for the marquee element.
 */
	setupMarqueeFx: function(){
		this.marqueeFx = new Fx.Marquee(this.marquee, this.options.marqueeFxOptions);
		//create an effect to hide the marquee when we show the title of things.
		this.titleMarqueeFx = new Fx.Morph(this.marquee);
	},
/*	Property: setupHelpTxt
		Internal; sets up the help text popup.
 */
	setupHelpTxt: function(){
				// Set up an iframe shim for the help balloon
				var helpShim = new IframeShim({
						element: $('whatsThisBox')
				});
				//set up the show links
		$$(this.options.helpLinks).each(function(el){
			el.addEvents({
				mouseenter: function(){
					el.setProperty('hovered', true);
					(function(){
												var hovered = el.getProperty('hovered');
						if(hovered=="true"||hovered===true) {
														$('whatsThisBox').set('html',
																this.options.helpText).setStyle('z-index', 9999).inject($(document.body)).setPosition({
																relativeTo: el,
																position: {
																																		x: 'center',
																																		y: 'top'
																																},
																edge: {
																																		x:'center',
																																		y: 'bottom'
																																},
																relFixedPosition: this.type=='affixed',
																offset: {
																		x: -55,
																		y: -7
																}
														}).show();
														helpShim.show();
														this.helpJlogger.ping();
												}
										}).delay(400, this);
				}.bind(this),
				mouseleave: function(){
					el.setProperty('hovered', false);
										(function(){
												var hovered = el.getProperty('hovered');
						if(hovered=="false"||hovered===false) $('whatsThisBox').hide();
										}).delay(400, this);
								}
								//click: this.helpSwin.show.bind(this.helpSwin)
			});
		}, this);
				this.addEvent('onDestroy', function(){
						if($('whatsThisBox')) $('whatsThisBox').remove();
				}.bind(this));
		},
/*	Property: buildIconMenu
		Internal; builds the instnace of <IconMenu>

		Arguments:
		collection - (array) passed along to <appendItems>
 */
	buildIconMenu: function(collection) {
				//create the function for setting the state of the scroll buttons
				var assertScrollImgs = function() {
						var show = {
								Right: this.calculateRange().end < this.imgs.length,
								Left: this.calculateRange().start != 0
						};
						['Left', 'Right'].each(function(side){
								$('scroll'+side).setStyles({
										cursor: show[side]?'pointer':'default',
										opacity: show[side]?1:0
								});
						});
				};
				//create the list element to store the icons.
		if(!this.listContainer) this.listContainer = new Element('ul').addClass('set').inject(this.container);
		//create an instance of IconMenu
		if(!this.iconMenu) {
			this.iconMenu = new IconMenu(
				$merge(this.options.iconMenuOptions, {
					container: this.listContainer,
										onScroll: assertScrollImgs,
										onAdd: assertScrollImgs,
										onRemoveItem: assertScrollImgs,
										onRemoveItems: assertScrollImgs
								})
			);
			//set up additional events
			this.iconMenu.addEvent('onFocus', this.showSubTitle.bind(this));
			this.iconMenu.addEvent('onBlur', function(){
				if(!this.iconMenu.inFocus) this.hideSubTitle()
			}.bind(this));
			this.iconMenu.addEvent('onEmpty', function(){
				this.data = [];
			}.bind(this));
		}
		//append items to the menu
		this.appendItems(collection);
		return this.iconMenu;
	},
/*	Property: appendItems
		Internal; appends items into the <IconMenu>.

		Arguments:
		collection - (array) an array of items with key/value properties for each icon for <IconMenu>

		Example:

(start code)
myProductToolbar.appendItems([
	{
		img: 'http://i.i.com.com/cnwk.1d/sc/32004258-2-60-0.gif',
		url:'index.html',
		txt: 'Nikon D80',
		id: 32004258
	},
	{
		img: 'http://i.i.com.com/cnwk.1d/sc/32069607-2-60-0.gif',
		url:'bbar2.html',
		txt: 'Canon PowerShot SD800 IS',
		id: 32069607
	}
]);
(end)
 */
	appendItems: function(collection){
		//if the collection is not an array, make it one
		if($type(collection) != "array") collection = [collection];
		//make an icon element with each item (data)
		var imgs = collection.map(function(item, index){
			return this.makeIconElement(item);
		}, this);
		//for each icon element...
		imgs.each(function(img){
			if(!img) return;
			//add a check box
			var checker = new Element('input', {type: 'checkbox'});
			//create a holder for the checkbox
			var caption = new Element('span').addClass('caption').adopt(checker);
						//set up the event so that when the input is changed, the IconMenu selects that icon
			checker.addEvent('click', function(){
				this.iconMenu.selectItem(this.iconMenu.images.indexOf(caption.retrieve('image')), checker.checked);
			}.bind(this));
						//insert the caption
			caption.inject(img,'after');
			//add the image and caption to the IconMenu
			this.iconMenu.addItem(img, caption, null, imgs.getLast()!=img);
		}, this);
		},
/*	Property: makeIconElement
		Internal; creates the image for the icon based on the data passed in.

		Arguments:
		item - (object) a data item (see <appendItems> for example>
		where - (integer) optional location where to put the icon; defaults to the end
 */
	makeIconElement: function(item, where){
		//create the placeholder image if it is not already there
		if(!$('noImageElement')) {
				new Element('img', {
						id: 'noImageElement',
						src: 'http://i.i.com.com/cnwk.1d/html/historytest/art/image_not_available.gif',
						styles: {
								visibility: 'hidden',
								position: 'absolute'
						}
				}).inject(document.body);
		}
		//get the children of the container that are list items
		var kids = this.iconMenu.container.getChildren().filter(function(el){return el.getTag()=="li"});
		//figure out where to put this new item
		where = $pick(where, kids.length);
		var hovered = false;
		var hoverTimeout;
		//create the image
		var img = new Element('img');
		img.set({
			src: item.img,
			//create a unique id for it; here I'm using the image file name
			id: item.id || item.img.substring(item.img.lastIndexOf('/')+1, item.img.lastIndexOf('.')),
			events: {
				//add a click event so the user can follow the url if they want to view the product
				click: function(){
					window.location.href = item.url+'?tag=box4505';
						PageVars.history.generalInteractionJlogger.ping();
					},
//								mouseover: function(){
//										hovered = true;
//										$clear(hoverTimeout);
//										hoverTimeout = (function(){
//												if (hovered) this.prodHoverLogger.ping();
//										}).delay(200, this);
//								}.bind(this),
//								mouseout: function(){
//										hovered = false;
//								},
								load: function(){
										var cImg = img.clone().setStyle('visibility', 'hidden').inject(document.body);
										cImg.style.width = '';
										cImg.style.height = '';
										if(cImg.width == 1) {
												var notFoundImg = $('noImageElement').clone().cloneEvents(img).setStyles({
														visibility: 'visible',
														width: 53,
														height: 40,
														marginTop: 0,
														marginBottom: 0,
														marginLeft: 3,
														marginRight: 3,
														padding: 3,
														position: 'relative'
												}).removeEvents('load').setProperty('id', img.getProperty('id'));
												var index = this.iconMenu.imgs.indexOf(img);
												if(index >= 0){
														notFoundImg.replaces(img);
														this.iconMenu.imgs[index] = notFoundImg;
														this.iconMenu.setupIconEvents(notFoundImg, this.iconMenu.captions[index]);
												}
										}
								}.bind(this)
						}
		});
		if($(img.id)) return false;
		//create a list item and insert the image into it
		var li = new Element('li').adopt(img);
				//add some hover effects to the li
				li.addEvent('mouseover', function(){
						li.addClass('hover');
						hovered = true;
						$clear(hoverTimeout);
						hoverTimeout = (function(){
								if (hovered) this.prodHoverLogger.ping();
						}).delay(200, this);
				}.bind(this));
				li.addEvent('mouseout', function(){
						hovered = false;
						if(!li.getElement('input').get('value')) li.removeClass('hover');
				}.bind(this));
				//put it into the body in the right spot
		if(kids[where])li.inject(kids[where],'before');
		else this.iconMenu.container.adopt(li);
				return img;
	},
/*	Property: makeSliderFx
		Internal; sets up the effect for toggling the bar in and out

		Arguments:
		transition - (function) reference to Fx.Transitions.*.* function to use for transition
 */
	makeSliderFx: function(transition){
		//if the slider doesn't exist, create it
		if(!this.slider && !Browser.Engine.trident4) this.slider = this.toolbarSliderContainer.effect('bottom', this.options.slideInFxOptions);
		else if (!this.slider) this.slider = this.toolbarSliderContainer.effect('margin-top', this.options.slideInFxOptions);
		//set the transition passed in
		if(transition) this.slider.setOptions({ transition: transition });
	}
});



/*	Class: EmbeddedProductToolbar
		Extends the ProductToolbar to allow the toolbar to be embedded into the page.

		Additional Options:
		maxCaptionLength - (integer) max number of characters to show in the subtitle before cropping and adding an ellipse.
		defaultTitle - (DOM element or id) dom element that references the title of the bar; used to show hover names
	*/
var EmbeddedProductToolbar = ProductToolbar.extend({
		type: 'embedded',
		options: {
		slideInPosition: 127, //default height for the open view
		slideOutPosition: 21, //default height for the closed view
		slideInPositionIE: 155, //default height for the open view in IE
		slideOutPositionIE: 21, //default height for the closed view in IE
		pin: false,
		defaultTitle: 'historyTitle',
		slideInFxOptions: {
			transition: Fx.Transitions.Quart.easeIn
		}
	},
	initialize: function(options){
		this.parent(options);
		this.defaultTitle = ($(this.options.defaultTitle))?
												$(this.options.defaultTitle).innerHTML.stripTags():
												this.options.defaultTitle;
		this.hideSubTitle();
		$('toolBoxOptions').adopt(this.subTitle);
		this.marqueeFx.options.showEffect = (options.marqueeFx)?options.marqueeFx.showEffect:{ opacity: [0,1] };
		this.marqueeFx.addEvent('onStart', function(){
			this.subTitle.hide();
		}.bind(this));
		this.marqueeFx.addEvent('onRevert', function(){
			this.subTitle.show();
		}.bind(this));
	},
	//create a different effect than ProductToolbar; we're using height instead of top to transition
	makeSliderFx: function(transition){
		if(!this.slider) this.slider = this.toolbarSliderContainer.effect('height', this.options.slideInFxOptions);
		this.slider.setOptions({
			transition: transition
		});
	},
/*	Property: showSubTitle
		Internal; shows the name on hover.

		Arguments:
		img - the img the user mouses over
 */
	showSubTitle: function(img){
				this.subTitleShown = img;
		this.subTitleFx.start(1);
		var range = this.iconMenu.calculateRange();
		var index = this.iconMenu.imgs.indexOf(img);
		//if the index of this icon is greater than .6 times the number of icons
		//in other words, farther right than just right of midpoint
		//put the name to the left of the icon
		var side = (index>(((range.start+4)-range.start)*.4)+range.start)?'Right':'Left';
		//dbug.log('index(%s) > end(%s)-start(%s)*.4 (%s): %s', index, (range.start+4), range.start, ((range.start+4-range.start)*.4)+range.start, side);
			var titleText = this.getItemByImgId(img.id).data.txt;
		if (titleText.length > 50) {
			titleText = titleText.substring(0,titleText.lastIndexOf(' ', 47)) + "...";
		}
		this.subTitle.empty().adopt(new Element('span', {
			'class':'subTitle',
			'styles':{
				'white-space': 'nowrap'
			}
		}).adopt(new Element('b').set('html',titleText))).adopt(
			new Element('img', {
				src: this.options.baseHref + '/tip_divot_y.gif',
				'class': 'tipDivot'+side
			})
		);
//				this.subTitle.setPosition({
//						relativeTo: img,
//						position: "center",
//						offset: {
//								y: 2-window.getHeight(),
//								x: (side == "Left")?-12:12
//						},
//						edge: "top"+side
//				});
//				(function(){
//			this.subTitleMover.start({
//				relativeTo: img,
//				position: "center",
//				offset: {
//					y: 2-window.getHeight(),
//					x: (side == "Left")?-12:12
//				},
//				edge: "top"+side
//			});
//		}).delay(100, this);
	},
	//redefine what it means to toggle; we're using height, not top
	toggle: function(e){
				if (e) new Event(e).stop();
				if(this.toolbarSliderContainer.getStyle('height').toInt() < 120) {
						this.slideIn();
						this.slideInLogger.ping();
				} else {
						this.slideOut();
						this.slideOutLogger.ping();
				}
	}
});


/* do not edit below this line */
/* Section: Change Log

$Source: /cvs/main/flatfile/html/rb/js/global/cnet.global.implementations/productToolbar/ProductToolbar.js,v $
$Log: ProductToolbar.js,v $
Revision 1.42	2008/02/27 22:51:18	kruegerw
Implementing logging to track any general interaction with the toolbox

Revision 1.41	2008/02/06 23:28:33	kruegerw
Making changes to adopt the 'b' version of the toolbox

Revision 1.39	2007/11/19 21:30:36	kruegerw
Put that y-offset back in there

Revision 1.38	2007/11/19 21:26:22	kruegerw
Making fixes for the recent products toolbox

Revision 1.37	2007/11/01 21:03:24	kruegerw
Checking in some fixes for Recently Viewed Products and the download profile page

Revision 1.36	2007/10/01 20:34:06	kruegerw
Set an instance variable to let us know if the toolbox has been scrolled

Revision 1.35	2007/09/21 21:37:52	kruegerw
Checking in changes for keeping the box collapsed when there is no history and adding some Jlogger stuff

Revision 1.34	2007/09/18 00:29:15	kruegerw
Forgot some semi-colons

Revision 1.33	2007/09/18 00:25:41	kruegerw
Adding more Jlogger stuff and an Iframe shim for the what's this bubble

Revision 1.32	2007/09/12 20:42:08	kruegerw
Adding one more Jlogger, removing some dbug statements and fixing some spacing issues

Revision 1.31	2007/09/12 20:30:41	kruegerw
Adding Jlogger pings for certain toolbox events

Revision 1.30	2007/09/11 00:17:39	kruegerw
Not sure why some toggle logic got removed in the last commit, putting it back in

Revision 1.29	2007/09/11 00:12:38	kruegerw
Need to change the baseHref to point at live artwork!

Revision 1.28	2007/09/05 23:57:10	kruegerw
Add some tags to a couple links

Revision 1.27	2007/09/04 18:43:56	mikem
Makes alert box display help text.

Revision 1.26	2007/09/04 18:23:38	newtona
Removed errant comma from ProductToolbar

Revision 1.25	2007/09/04 17:35:13	newtona
Fixed a bug in IconMenu where, if removeLinks and clearLinks were not defined, the behavior was applied to all dom elements (doh!)
Rearranged ProductToolbar's references to these options so that, rather than setting up these behaviors itself it passes them on to IconMenu to do it.

Revision 1.24	2007/09/04 15:51:39	andyl
Adding punctuation so compressor doesn't choke.

Revision 1.23	2007/08/31 23:36:12	mikem
superfluous bold tags

Revision 1.22	2007/08/31 22:35:01	mikem
Removes "Show on every page" from help text.

Revision 1.21	2007/08/31 22:32:58	mikem
Minor alt-text change.

Revision 1.20	2007/08/31 18:59:02	mikem
Removes sliding and fading of subTitles, and ups max char count before truncation.

Revision 1.19	2007/08/31 17:31:48	mikem
First round of cosmetic updates.

Revision 1.18	2007/08/30 05:57:40	mikem
Minor clean-up.

Revision 1.17	2007/08/30 04:55:20	mikem
Makes compare link relative for UAT.

Revision 1.16	2007/08/30 00:27:39	andyl
Rearranged some of the furniture

Revision 1.15	2007/08/29 22:36:12	mikem
Final fix and offset values.

Revision 1.14	2007/08/29 20:50:14	andyl
Updated to fix product title placement, replace image links with text.

Revision 1.13	2007/08/29 19:37:36	andyl
Updating to swap out show on every page and close links for text, rather than images; should have fixed the titles on the toolbox version

Revision 1.12	2007/08/29 19:20:28	mikem
adds onDestroy for Whats This.

Revision 1.11	2007/08/29 18:53:03	andyl
Changed placement of the scroll arrows in the HTML

Revision 1.10	2007/08/29 17:19:25	mikem
setupHelpTxt fixes.

Revision 1.9	2007/08/29 03:19:24	mikem
HTML changes.

Revision 1.8	2007/08/29 00:38:12	andyl
Added some embedded version specific logic for showing sub titles

Revision 1.7	2007/08/28 21:32:25	mikem
HTML changes.

Revision 1.6	2007/08/28 21:17:59	mikem
Adds relFixedPosition option to setupHelpTxt.

Revision 1.5	2007/08/28 20:24:21	mikem
What's this updates.

Revision 1.4	2007/08/28 18:09:46	mikem
Removes css for What's this

Revision 1.3	2007/08/27 23:37:24	mikem
Minor updates to intro text.

Revision 1.2	2007/08/27 19:08:06	mikem
Adds hideLink option so Close link hides in place instead of swapping.

Revision 1.1	2007/08/25 00:05:35	newtona
moved ProductToolbar to global implementations
handled ie6 slightly differently in fixPNG, added some dbug lines for when it failes
updated commerce global cat file for new location of ProductToolbar
rebuilt redball.common.full

Revision 1.7	2007/08/24 23:45:33	andyl
Updated to add some changes to the default innerHTML as per Mike Ruane; added a slideInPositionIE and sildeOutPositionIE for the embedded product tool bar

Revision 1.6	2007/08/24 23:26:12	mikem
New What's This treatment for toolbox.

Revision 1.5	2007/08/24 21:35:05	mikem
First rev, new What's This treatment.

Revision 1.4	2007/08/22 01:00:45	kruegerw
Removing remnant of line that should have been deleted

Revision 1.3	2007/08/21 22:37:39	kruegerw
Implement coco link in compareSelected function

Revision 1.2	2007/08/21 00:58:09	newtona
RTSS.History, RTSS.JsonP: added events for add, remove, empty, etc.
RTSS.js: most methods now return the remote class (XHR or JsonP)
UserHistory: add logic to announce actions (add, remove, etc.)
ProductToolbar: implemented place-holder compare function; removed some dbug lines
JsonP: tweaking retry logic

Revision 1.1	2007/08/20 21:20:44	newtona
first big check in for RTSS History


*/
/*	
Script: HistoryBar.Init.js
	Initializes the User History toolbar for CNET commerce sites.
	
License:
	No License; internal CNET code not intended for reuse.
	*/
window.addEvent('domready', function() {
//	try {
	CNETAPI.register('userHistory', {
    apiUrl: 'http://internal-api.cnet.com/restApi/v1.0',
    requestUrl: 'http://reviews.cnet.com/9801-4_7-0-3.html'
	});
	var validPageTypes = [4505,4852,4864,4507,4540,4510,4014,4566,1764,1707,1724,1753];
	PageVars.history = new UserHistory({});
	(function(){
		if(validPageTypes.contains(PageVars.get('pageType')-0))
			PageVars.history.add(PageVars.get('assetId')-0);
		else PageVars.history.getHistory();
	}).delay(1000);
	// DW Data tracking
	var impressionPinger = function(){
		var bar = PageVars.history.bar;
		var numProds = bar.data.length;
		if(numProds > 1) bar.impTwoJlogger.ping();
		if(numProds == 1) bar.impOneJlogger.ping();
		if(numProds < 1) bar.impZeroJlogger.ping();
	};
	PageVars.history.addEvent('onDataLoad', function(){
		impressionPinger();
		PageVars.history.removeEvent('onDataLoad', impressionPinger);
	});
	PageVars.history.rtss.addEvent('onFailure', function(){
		impressionPinger();
		PageVars.history.rtss.removeEvent('onFailure', impressionPinger);
	});
	// Switch out loading GIF for actual content
	var content = '<img src="http://i.i.com.com/cnwk.1d/html/historytest/art/zleft.gif" alt="scroll left" id="scrollLeft" width="8" height="70" style="visibility: hidden"/>'+
		'<div id="inBarMsg">When you look at products on CNET, we\'ll keep track of them here.</div>'+
		'<img src="http://i.i.com.com/cnwk.1d/html/historytest/art\/zright.gif" alt="scroll right" id="scrollRight" width="8" height="70" style="visibility: hidden"/>';
		PageVars.history.addEvent('onDataLoad', function(){
		if($('loadingImage')) $E('.set').replaces($('loadingImage'));
	});
	new Jlogger({
		ctype: 'tooltype;action',
		cval: 'box;rm-all',
		tag: 'box-rm-all',
		element: 'clearall',
		event: 'click',
		onPing: function(){
				PageVars.history.generalInteractionJlogger.ping();
		}
	});
	new Jlogger({
		ctype: 'tooltype;action',
		cval: 'box;remove',
		tag: 'box-remove',
		element: 'removeSelected',
		event: 'click',
		onPing: function(){
				PageVars.history.generalInteractionJlogger.ping();
		}
	});
	// Ping DW when someone scrolls to the left in their history
	var leftScrollJlogger = new Jlogger({
		ctype: 'tooltype;action',
		cval: 'box;scrllft',
		tag: 'box-scroll-left',
		fireOnce: true,
		onPing: function(){
				PageVars.history.generalInteractionJlogger.ping();
		}
	});
	// Ping DW when someone scrolls to the right in their history
	var rightScrollJlogger = new Jlogger({
		ctype: 'tooltype;action',
		cval: 'box;scrllrght',
		tag: 'box-scroll-right',
		fireOnce: true,
		onPing: function(){
				PageVars.history.generalInteractionJlogger.ping();
		}
	});
	// Ping DW when any interaction occurs, only once
	PageVars.history.generalInteractionJlogger = new Jlogger({
		ctype: 'tooltype;action',
		cval: 'box;general',
		tag: 'box-general',
		fireOnce: true
	});
//				PageVars.history.generalHoverLogger = new Jlogger({
//						ctype: 'tooltype;action',
//						cval: 'box;general',
//						tag: 'box-general',
//						element: 'toolboxHistory',
//						fireOnce: true
//				});
/*
	$('scrollLeft').addEvent('click', function(){
		if (PageVars.history.bar.iconMenu.scrolled) {
			leftScrollJlogger.ping();
		} else {
			return false;
		}
	});
	$('scrollRight').addEvent('click', function(){
		if (PageVars.history.rtss.data.offset < (PageVars.history.rtss.data.totalProducts - 4)) {
			rightScrollJlogger.ping();
		} else {
			return false;
		}
	});
*/
/*			} catch(e) {
		dbug.log('Error setting	up history bar: ', e);
	}	*/
});