/*

unbad object library Copyright © 2006-2010 - Sean Claflin

unbad object library is licensed under the MIT License
http://www.opensource.org/licenses/mit-license.php

*/

(ub.widget = {
	init: function() {
		ub.e.attach(ub, 'unload', function(e) {
			this.destruct();
		}, false, this);
		ub.require('fx');
	},
	destruct: function() {
		ub.a.each(this.events, function(event) {
			if(event.detach)
				event.detach();
		}, this, false);
		ub.a.clear(this.events);
		ub.a.each(this.widgets, function(widget) {
			if(widget.destruct)
				widget.destruct();
		}, this, true);
		ub.a.clear(this.widgets);
	},
	events: [],
	widgets: [],
	//simple stretcher object
	stretch: function() {
		var stretch = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null
			}, options || {});
			this.events = [];
			//create nodes
			this.stretcher = document.createElement('div');
			this.container = document.createElement('div');
			this.mask = document.createElement('div');
			//build hierarchy
			this.stretcher.appendChild(this.container);
			this.stretcher.appendChild(this.mask);
			//add classNames
			this.container.className = 'ubStretcher';
			if(this.options.id) this.container.id = this.options.id;
			this.mask.style['display'] = 'none';
			this.mask.style['background'] = 'transparent';
			//set FX
			this.height = new ub.fx.height(this.stretcher);
			//attach to events so we can account for a display bug in FireFox 3
			ub.a.append(this.events, ub.e.attach(this.height, 'hide', function(e) {
				this.stretcher.style['display'] = 'none';
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.height, 'show', function(e) {
				this.stretcher.style['display'] = 'block';
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.height, 'expose', function(e) {
				if(!this.height.exposed())
					this.stretcher.style['display'] = 'block';
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.height, 'start', function(e) {
				this.mask.style['display'] = 'block';
				ub.n.setDims(this.mask, ub.n.getDims(this.container));
				this.container.style['display'] = 'none';
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.height, 'finish', function(e) {
				this.mask.style['display'] = 'none';
				this.container.style['display'] = 'block';
				if(!this.height.exposed())
					this.stretcher.style['display'] = 'none';
			}, false, this));
		};
		stretch.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.stretcher = null;
			this.container = null;
			this.mask = null;
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			this.height.destruct();
			this.destruct = function() {};
		};
		return stretch;
	}(),
	//simple "bar" object
	bar: function() {
		var bar = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				title: 'Default Title'
			}, options || {});
			//create nodes
			this.container = document.createElement('div');
			this.title = document.createElement('div');
			this.buttons = document.createElement('div');
			//build hierarchy
			this.container.appendChild(this.buttons);
			this.container.appendChild(this.title);
			//add classNames
			this.container.className = 'ubBarContainer';
			this.title.className = 'ubBarTitle';
			this.buttons.className = 'ubBarButtons';
			
			this.title.innerHTML = this.options.title;
			if(this.options.id) this.container.id = this.options.id;
		};
		bar.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			this.buttons = null;
			this.title = null;
			this.destruct = function() {};
		};
		return bar;
	}(),
	//combines bar and stretcher to make a folding object
	fold: function() {
		var fold = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null
			}, options || {});
			//create a container node
			this.container = document.createElement('div');
			this.container.className = 'ubFold';
			if(this.options.id) this.container.id = this.options.id;
			
			this.bar = new ub.widget.bar();
			this.stretcher = new ub.widget.stretch();
			//stuff the bar and stretcher into our container
			this.container.appendChild(this.bar.container);
			this.container.appendChild(this.stretcher.stretcher);
			//add a click handler to the stretcher
			this.barClick = ub.e.attach(this.bar.container, 'click', function(e) {
				this.stretcher.height.toggle();
			}, false, this);
		};
		fold.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			this.bar.destruct();
			this.bar = null;
			this.stretcher.destruct();
			this.stretcher = null;
			this.barClick.detach();
			this.destruct = function() {};
		};
		return fold;
	}(),
	//combines any number of fold objects into an accordion
	accordion: function() {
		var accordion = function(options) {
			this.options = ub.u.extend({
				id: null,
				folds: 3,
				initialOpenFold: 0
			}, options || {});
			//create a container node
			this.container = document.createElement('div');
			this.container.className = 'ubAccordion';
			if(this.options.id) this.container.id = this.options.id;
			this.folds = [];
			this.events = [];
			//build the required folds
			for(var i=0;i<this.options.folds;i++)
				ub.a.append(this.folds, new ub.widget.fold());
			//loop through the folds we just made and make some changes
			ub.a.each(this.folds, function(fold, i) {
				this.container.appendChild(fold.container);
				if(i != this.options.initialOpenFold)
					fold.stretcher.height.hide();
				//override default click behavior
				fold.barClick.detach();
				fold.barClick = ub.e.attach(fold.bar.container, 'click', function(e) {
					this.toggle(fold);
				}, false, this);
			}, this)
		};
		accordion.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			ub.a.each(this.folds, function(fold) {
				fold.destruct();
			})
			ub.a.clear(this.folds);
			this.destruct = function() {};
		};
		accordion.prototype.toggle = function(fold) {
			ub.a.each(this.folds, function(_fold) {
				if(_fold == fold) {
					_fold.stretcher.height.expose();
					this.onexpose(new ub.e.event('expose'), fold);
				}
				else {
					_fold.stretcher.height.conceal();
					this.onconceal(new ub.e.event('conceal'), fold);
				}
				this.ontoggle(new ub.e.event('toggle'), fold);
			}, this);
		};
		accordion.prototype.onexpose = function(e, fold) {};
		accordion.prototype.onconceal = function(e, fold) {};
		accordion.prototype.ontoggle = function(e, fold) {};
		return accordion;
	}(),
	//modal dialog box
	dialog: function() {
		var dialog = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				zIndex: 10000
			}, options || {});
			this.events = [];
			this.buttons = [];
			this.container = document.createElement('div');
			ub.n.setOpacity(this.container, 0);
			
			this.messageContainer = document.createElement('div');
			this.container.appendChild(this.messageContainer);
			this.messageContainer.className = 'ubDialogMessageContainer';
			this.messageContainer.style['position'] = 'fixed';
			this.messageContainer.style['width'] = '100%';
			this.messageContainer.style['height'] = '100%';
			this.messageContainer.style['left'] = '0';
			this.messageContainer.style['top'] = '0';
			this.messageContainer.style['zIndex'] = this.options.zIndex + 1;
			
			this.messageOuter = document.createElement('div');
			this.messageContainer.appendChild(this.messageOuter);
			this.messageOuter.className = 'ubDialogMessageOuter';
			
			this.message = document.createElement('div');
			this.messageOuter.appendChild(this.message);
			this.message.className = 'ubDialogMessage';
			
			this.mask = document.createElement('div');
			this.container.appendChild(this.mask);
			this.mask.className = 'ubDialogMask';
			this.mask.style['position'] = 'fixed';
			this.mask.style['width'] = '100%';
			this.mask.style['height'] = '100%';
			this.mask.style['left'] = '0';
			this.mask.style['top'] = '0';
			this.mask.style['zIndex'] = this.options.zIndex;
			
			if(this.options.id) this.message.id = this.options.id;
			this.opacity = new ub.fx.opacity(this.container);
		};
		dialog.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			this.mask = null;
			this.messageContainer = null;
			this.messageOuter = null;
			this.message = null;
			this.opacity.destruct();
			this.opacity = null;
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			this.destruct = function() {};
		};
		dialog.prototype.expose = function(msg, buttons, callback, scope) {
			this.conceal();
			var btnBar = document.createElement('div');
			btnBar.className = 'ubDialogButtonBar';
			//capture keyboard events
			this.down = ub.e.attach(document, 'keydown', function(e) {
				//close the dialog if escape is pressed
				if(e.keyCode == '27') {
					this.conceal(-1);
					if(typeof(callback) == 'function')
						callback.call(scope, -1, this.buttons);
				}
				//allow keystrokes on nodes within the container element
				else if(ub.n.contains(this.container, e.target)) {
					return true;
				}
				//squelch everything else
				return false;
			}, true, this, true);
			this.up = ub.e.attach(document, 'keyup', function(e) {
				return false;
			}, true, this, true);
			
			//make sure a scope is set
			if(!scope) scope = window;
			//make sure the mask is not visible
			this.container.style['visibility'] = 'hidden';
			//run through the passed buttons, add them to the div
			ub.a.each(buttons, function(button, i) {
				var btn = ub.a.append(this.buttons, document.createElement('button'));
				btn.className = 'ubDialogButton';
				btn.innerHTML = button;
				//set a click handler for each button
				btn.click = ub.e.attach(btn, 'click', function(e) {
					this.conceal(i);
					if(typeof(callback) == 'function')
						callback.call(scope, i, this.buttons);
				}, false, this);
				//add button to our container
				btnBar.appendChild(btn);
				btn = null;
			}, this);
			//remove any prior dialog contents
			ub.n.removeChildren(this.message);
			//add our text and button bar
			this.message.appendChild(msg);
			this.message.appendChild(btnBar);
			//add it to the body
			document.body.appendChild(this.container);
			//expose!
			this.opacity.halt();
			this.opacity.expose();
		};
		dialog.prototype.conceal = function(value) {
			if(this.down) this.down.detach();
			if(this.up) this.up.detach();
			//you go now!
			this.opacity.halt();
			this.opacity.conceal();
			//clean up our button events and buttons
			ub.a.each(this.buttons, function(btn) {
				btn.click.detach();
				btn.click = null;
			});
			ub.a.clear(this.buttons);
			//closure
			//var container = this.container;
			//remove the dialog after it's animation has ended
			ub.u.setTimeout(function() {
				if(this.opacity.exposed()) return;
				if(this.container.parentNode) {
					this.container.parentNode.removeChild(this.container);
				}
			}, this.opacity.options.duration + 100, this);
			return value;
		};
		return dialog;
	}(),
	//slide control
	slider: function() {
		var slider = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				scale: [0,100],
				containerClass: 'ubSliderContainer',
				boundaryClass: 'ubSliderBoundary',
				controlClass: 'ubSliderControl'
			}, options || {});
			this.events = [];
			//create our container
			this.container = document.createElement('div');
			this.container.className = this.options.containerClass;
			if(this.options.id) this.container.id = this.options.id;
			
			this.boundary = document.createElement('div');
			this.boundary.style['padding'] = '0px';
			this.boundary.style['border'] = 'none';
			this.boundary.className = this.options.boundaryClass;
			this.container.appendChild(this.boundary);
			
			this.control = document.createElement('div');
			this.control.style['position'] = 'relative';
			this.control.className = this.options.controlClass;
			this.boundary.appendChild(this.control);
			
			this.label = document.createElement('div');
			this.container.appendChild(this.label);
			
			this.dragable = new ub.mouse.dragable(this.control, { restrictContainer: true });
			this.position = new ub.fx.position(this.control, { easing: ub.fx.easing.easeOut });
			ub.a.append(this.events, ub.e.attach(this.dragable, 'dragmove', function(e) {
				this.update();
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.boundary, 'click', function(e) {
				if(e.target == this.boundary)
					this.slideTo(e.coords);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.position, 'update', function(e) {
				this.update();
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.position, 'jump', function(e) {
				this.update();
			}, false, this));
		};
		slider.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			this.boundary = null;
			this.control = null;
			this.label = null;
			this.dragable.destruct();
			this.dragable = null;
			this.position.destruct();
			this.position = null;
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			this.destruct = function() {};
		};
		slider.prototype.update = function() {
			var bCoords = ub.n.getCoords(this.boundary);
			var cCoords = ub.n.getCoords(this.control);
			var bDims = ub.n.getDims(this.boundary);
			var cDims = ub.n.getDims(this.control);
			var max = bDims[0] - cDims[0];
			var pos = cCoords[0] - bCoords[0];
			this.onslide(new ub.e.event('slide'), Math.round(pos/max * 10000)/10000);
		};
		slider.prototype.slideTo = function(coords) {
			var cDims = ub.n.getDims(this.control);
			var cOffset = ub.n.realOffset(this.control);
			var bDims = ub.n.getDims(this.boundary);
			var bOffset = ub.n.realOffset(this.boundary);
			var bCoords = ub.n.getCoords(this.boundary);
			var delta = coords[0] - bOffset[0] - cDims[0]/2;
			//adjust delta to stay within bounds
			if(delta < 0)
				delta = 0;
			else if(delta > bDims[0] - cDims[0])
				delta = bDims[0] - cDims[0];
			this.position.move([bCoords[0] + delta, bCoords[1]]);
			var slider = this;
		},
		slider.prototype.setValue = function(value, slide) {
			//force a positive number
			if((value = Math.abs(value)) > 1) throw new Error('Value must be >= 0 and <= 1');
			var cDims = ub.n.getDims(this.control);
			var cCoords = ub.n.getCoords(this.control);
			var bDims = ub.n.getDims(this.boundary);
			
			//get the desired X coord for this value
			var newX = (bDims[0] - cDims[0]) * value;
			
			if(!slide)
				this.position.jump([newX, cCoords[1]]);
			else
				this.position.move([newX, cCoords[1]]);
		}
		slider.prototype.onslide = function(e, value) {};
		return slider;
	}(),
	menu: function() {
		var menu = function(ul, options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				zIndex: 10000
			}, options || {});
			this.ul = ul;
			this.events = [];
			this.subMenus = [];
			this.activeLi = null;
			this.inactiveTimer = null;
			this.makeSubMenus(this.ul);
		}
		menu.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			ub.a.each(this.subMenus, function(subMenu) {
				if(subMenu.slide.container)
					document.body.removeChild(subMenu.slide.container);
				subMenu.pNode = null;
				subMenu.li = null;
				subMenu.ul = null;
				subMenu.slide.destruct();
				subMenu.slide = null;
			});
			ub.a.clear(this.subMenus);
			this.destruct = function() {};
		};
		menu.prototype.makeSubMenus = function(pNode) {
			ub.a.each(this.getLis(pNode), function(li) {
				ub.a.each(this.getUls(li), function(ul) {
					//automagically add the ubSubMenu class
					ub.n.addClassName(ul, 'ubSubMenu');
					var subMenu = ub.a.append(this.subMenus, {
						pNode: pNode,
						li: li,
						ul: ul,
						slide: new ub.fx.slide(ul, { easing: ub.fx.easing.easeOut, duration: 200 })
					});
					document.body.appendChild(subMenu.slide.container);
					subMenu.slide.container.style['position'] = 'absolute';
					subMenu.slide.container.style['zIndex'] = this.options.zIndex;
					subMenu.slide.outLeft();
					subMenu.slide.container.style['display'] = 'none';
					this.makeSubMenus(ul);
				}, this);
				ub.a.append(this.events, ub.e.attach(li, 'mouseover', function(e) {
					this.activeLi = li;
					if(this.inactiveTimer) {
						clearTimeout(this.inactiveTimer);
						this.inactiveTimer = null;
					}
					this.updateMenus(e, li);
				}, false, this));
				ub.a.append(this.events, ub.e.attach(li, 'mouseout', function(e) {
					if(this.activeLi == li) {
						this.activeLi = null;
						//closure
						var menu = this;
						this.inactiveTimer = setTimeout(function() {
							menu.inactiveTimer = null;
							if(!menu.activeLi) {
								ub.a.each(menu.subMenus, function(subMenu) {
									menu.hideSubMenu(subMenu);
								});
							}
							menu = null;
						}, 1000);
					}
				}, false, this));
			}, this);
		};
		menu.prototype.updateMenus = function(e, li) {
			var ul = li.parentNode;
			ub.a.each(this.subMenus, function(subMenu) {
				if(subMenu.li == li) {
					this.showSubMenu(subMenu);
				}
				else if(subMenu.pNode == ul) {
					this.hideSubMenu(subMenu);
				}
			}, this);
		};
		menu.prototype.getParentMenu = function(subMenu) {
			var parentMenu = null;
			ub.a.each(this.subMenus, function(_subMenu) {
				if(_subMenu.ul == subMenu.pNode) {
					parentMenu = _subMenu;
				}
			});
			return parentMenu;
		};
		menu.prototype.isAncestorMenu = function(ancestorMenu, subMenu) {
			var _subMenu = subMenu;
			while(_subMenu = this.getParentMenu(_subMenu)) {
				if(_subMenu == ancestorMenu)
					return true;
			}
			return false;
		};
		menu.prototype.isSiblingMenu = function(subMenu1, subMenu2) {
			return (subMenu1.pNode == subMenu2.pNode);
		};
		menu.prototype.showSubMenu = function(subMenu) {
			var container = subMenu.slide.container;
			container.style['display'] = 'block';
			var liCoords = ub.n.realOffset(subMenu.li);
			var liDims = ub.n.getDims(subMenu.li);
			var ulDims = ub.n.getDims(subMenu.ul);
			var clearances = this.getClearances(subMenu);
			
			//add the subMenuActive class
			ub.n.addClassName(subMenu.li, 'ubSubMenuActive');
			
			//check for vertical orientation
			if(ub.n.hasClassName(subMenu.pNode, 'ubMenuV')) {
				container.style['left'] = liCoords[0] +'px';
				if(!clearances.bottom && clearances.top)
					container.style['top'] = (liCoords[1] - ulDims[1]) +'px';
				else
					container.style['top'] = (liCoords[1] + liDims[1]) +'px';
			}
			else {
				if(!clearances.bottom && clearances.top)
					container.style['top'] = (liCoords[1] + liDims[1] - ulDims[1]) +'px';
				else
					container.style['top'] = liCoords[1] +'px';
				if(!clearances.right && clearances.left)
					container.style['left'] = (liCoords[0] - ulDims[0]) +'px';
				else
					container.style['left'] = (liCoords[0] + liDims[0]) +'px';
			}
			//check for a timer, halt if we're sliding out
			if(subMenu.slide.timer && subMenu.slide.state == 'out')
				subMenu.slide.halt();
			//make sure there's no timer and we were previously slid out
			if(!subMenu.slide.timer && subMenu.slide.state == 'out') {
				if(ub.n.hasClassName(subMenu.pNode, 'ubMenuV')) {
					if(!clearances.bottom && clearances.top)
						subMenu.slide.inBottom();
					else
						subMenu.slide.inTop();
				}
				else {
					if(!clearances.right && clearances.left)
						subMenu.slide.inRight();
					else
						subMenu.slide.inLeft();
				}
			}
		};
		menu.prototype.hideSubMenu = function(subMenu) {
			ub.a.each(this.subMenus, function(_subMenu) {
				if(this.isAncestorMenu(subMenu, _subMenu))
					this.hideSubMenu(_subMenu);
			}, this);
			//do nothing if not exposed
			if(!subMenu.slide.exposed()) return;
			//do nothing if we're already hiding
			if(subMenu.slide.timer && subMenu.slide.state == 'out') return;
			//we're obviously exposed and not sliding out so cancel any running
			//animation and slide out
			var clearances = this.getClearances(subMenu);
			//add the subMenuActive class
			ub.n.removeClassName(subMenu.li, 'ubSubMenuActive');
			subMenu.slide.halt();
			if(ub.n.hasClassName(subMenu.pNode, 'ubMenuV')) {
				if(!clearances.bottom && clearances.top)
					subMenu.slide.outBottom();
				else
					subMenu.slide.outTop();
			}
			else {
				if(!clearances.right && clearances.left)
					subMenu.slide.outRight();
				else
					subMenu.slide.outLeft();
			}
			setTimeout(function() {
				if(!subMenu.slide.exposed())
					subMenu.slide.container.style['display'] = 'none';
			}, subMenu.slide.options.duration + 10);
		};
		menu.prototype.getClearances = function(subMenu) {
			var viewport = ub.u.viewportDims();
			var liCoords = ub.n.realOffset(subMenu.li);
			var liDims = ub.n.getDims(subMenu.li);
			var ulDims = ub.n.getDims(subMenu.ul);
			var clearances = {
				right: false,
				left: false,
				top: false,
				bottom: false
			};
			if(ulDims[0] <= viewport[0] - liCoords[0] - liDims[0])
				clearances.right = true;
			if(ulDims[0] <= liCoords[0])
				clearances.left = true;
			if(ulDims[1] <= viewport[1] - liCoords[1] - liDims[1])
				clearances.bottom = true;
			if(ulDims[1] <= liCoords[1])
				clearances.top = true;
			return clearances;
		};
		menu.prototype.getUls = function(pNode) {
			var uls = [];
			ub.a.each(ub.$c(pNode.childNodes), function(childNode) {
				if(childNode.nodeName.toLowerCase() == 'ul')
					ub.a.append(uls, childNode);
			});
			return uls;
		};
		menu.prototype.getLis = function(pNode) {
			var lis = [];
			ub.a.each(ub.$c(pNode.childNodes), function(childNode) {
				if(childNode.nodeName.toLowerCase() == 'li')
					ub.a.append(lis, childNode);
			});
			return lis;
		};
		return menu;
	}(),
	tab: function() {
		var tab = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				title: 'Default Title',
				containerClass: 'ubTabContainer',
				tabActiveClass: 'ubTabActive'
			}, options || {});
			this.events = [];
			this.container = document.createElement('div');
			this.container.className = this.options.containerClass;
			this.container.innerHTML = this.options.title;
			if(this.options.id) this.container.id = this.options.id;
			ub.a.append(this.events, ub.e.attach(this.container, 'click', function(e) {
				this.onclick(e, this);
			}, false, this));
		};
		tab.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			this.destruct = function() {};
		};
		tab.prototype.setActive = function(active) {
			if(active) ub.n.addClassName(this.container, this.options.tabActiveClass);
			else ub.n.removeClassName(this.container, this.options.tabActiveClass);
		};
		tab.prototype.onclick = function(e, tab) {};
		return tab;
	}(),
	tabBar: function() {
		var tabBar = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				containerClass: 'ubTabBarContainer',
				tabClass: 'ubTabContainer',
				tabActiveClass: 'ubTabActive',
				tabs: ['Default Tab']
			}, options || {});
			this.events = [];
			this.tabs = [];
			this.container = document.createElement('div');
			this.container.className = this.options.containerClass;
			if(this.options.id) this.container.id = this.options.id;
			//add tabs
			for(var i=0;i<this.options.tabs.length;i++) {
				this.addTab(this.options.tabs[i]);
			}
		};
		tabBar.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			ub.a.each(this.tabs, function(tab) {
				if(tab.destruct) tab.destruct();
			});
			this.container = null;
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			this.destruct = function() {};
		};
		tabBar.prototype.getTab = function(title) {
			for(var i=0;i<this.tabs.length;i++) {
				if(this.tabs[i].options.title == title)
					return this.tabs[i];
			}
			return false;
		};
		tabBar.prototype.addTab = function(title) {
			var tab = new ub.widget.tab({ 
				title: title,
				containerClass: this.options.tabClass,
				tabActiveClass: this.options.tabActiveClass
			});
			this.container.appendChild(tab.container);
			ub.a.append(this.tabs, tab);
			tab.clickHandler = ub.a.append(this.events, ub.e.attach(tab, 'click', function(e, tab) {
				for(var i=0;i<this.tabs.length;i++)
					this.tabs[i].setActive(this.tabs[i] == tab);
				this.ontabactive(e.chType('tabactive'), tab);
			}, false, this));
		};
		tabBar.prototype.removeTab = function(tab, destruct) {
			this.container.removeChild(tab.container);
			ub.a.remove(this.tabs, tab);
			ub.a.remove(this.events, tab.clickHandler);
			tab.clickHandler.detach();
			tab.clickHandler = null;
			if(destruct)
				tab.destruct();
		};
		tabBar.prototype.ontabactive = function(e, tab) {};
		return tabBar;
	}(),
	tabControl: function() {
		var tabControl = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				containerClass: 'ubTabControlContainer',
				tabControlContentClass: 'ubTabControlContent',
				tabBarClass: 'ubTabBarContainer',
				tabClass: 'ubTabContainer',
				tabActiveClass: 'ubTabActive',
				tabs: [{ id: 'default_tab', title: 'Default Tab'}]
			}, options || {});
			this.events = [];
			this.container = document.createElement('div');
			this.container.className = this.options.containerClass;
			if(this.options.id) this.container.id = this.options.id;
			//add tab bar
			this.tabBar = new ub.widget.tabBar({
				containerClass: this.options.tabBarClass,
				tabClass: this.options.tabClass,
				tabActiveClass: this.options.tabActiveClass,
				tabs: this.options.tabs
			});
			this.container.appendChild(this.tabBar.container);
			this.content = document.createElement('div');
			this.content.className = this.options.tabControlContentClass;
			this.container.appendChild(this.content);
			ub.a.append(this.events, ub.e.attach(this.tabBar, 'tabactive', function(e, tab) {
				this.ontabactive(e, tab);
			}, false, this));
			//set the default tab
			if(this.tabBar.tabs[0])
				this.tabBar.tabs[0].setActive(true);
		};
		tabControl.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			this.content = null;
			this.tabBar.destruct();
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			});
			ub.a.clear(this.events);
			this.destruct = function() {};
		};
		tabControl.prototype.ontabactive = function(e, tab) {};
		return tabControl;
	}(),
	dataGrid: function() {
		var dataGrid = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				pageLength: 20,
				records: [],
				downloadUri: ''
			}, options || {});
			this.events = [];
			this.recordEvents = [];
			this.fx = {};
			this.descending = false;
			this.pageNum = 0;
			this.sortField = '';
			this.headers = [];
			this.records = this.options.records;
			
			this.container = document.createElement('div');
			this.container.className = 'ubDataGridContainer';
			
			this.pageBarContainer = document.createElement('div');
			this.pageBarContainer.className = 'ubDataGridPageBarContainer';
			this.container.appendChild(this.pageBarContainer);
			
			this.pageBarLeft = document.createElement('div');
			this.pageBarLeft.className = 'ubDataGridPageBarLeft';
			this.pageBarContainer.appendChild(this.pageBarLeft);
			
			this.pageBarFirst = document.createElement('div');
			this.pageBarFirst.className = 'ubDataGridPageBarFirst';
			this.pageBarFirst.title = 'First record';
			this.pageBarLeft.appendChild(this.pageBarFirst);
			
			this.pageBarPrev = document.createElement('div');
			this.pageBarPrev.className = 'ubDataGridPageBarPrev';
			this.pageBarPrev.title = 'Prior record';
			this.pageBarLeft.appendChild(this.pageBarPrev);
			
			this.pageBarNext = document.createElement('div');
			this.pageBarNext.className = 'ubDataGridPageBarNext';
			this.pageBarNext.title = 'Next record';
			this.pageBarLeft.appendChild(this.pageBarNext);
			
			this.pageBarLast = document.createElement('div');
			this.pageBarLast.className = 'ubDataGridPageBarLast';
			this.pageBarLast.title = 'Last Record';
			this.pageBarLeft.appendChild(this.pageBarLast);
			
			if(this.options.downloadUri) {
				this.pageBarSave = document.createElement('div');
				this.pageBarSave.className = 'ubDataGridPageBarSave';
				this.pageBarSave.title = 'Download';
				this.pageBarContainer.appendChild(this.pageBarSave);
			}
			
			this.pageBar = document.createElement('div');
			this.pageBar.className = 'ubDataGridPageBar';
			this.pageBarContainer.appendChild(this.pageBar);
			
			this.table = document.createElement('table');
			this.table.className = 'ubDataGridTable';
			this.container.appendChild(this.table);
			
			this.thead = document.createElement('thead');
			this.thead.className = 'ubDataGridThead';
			this.table.appendChild(this.thead);
			
			this.tfoot = document.createElement('tfoot');
			this.tfoot.className = 'ubDataGridTfoot';
			this.table.appendChild(this.tfoot);
			
			this.tbody = document.createElement('tbody');
			this.tbody.className = 'ubDataGridTbody';
			this.table.appendChild(this.tbody);
			
			this.fx.scroll = new ub.fx.scroll(this.pageBar);
			
			ub.a.append(this.events, ub.e.attach(this.pageBarFirst, 'click', function(e) {
				var page = 0;
				this.showPage(page);
				this.updatePageBar();
				this.scrollToPage(page);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.pageBarPrev, 'click', function(e) {
				var page = (this.pageNum > 0) ? this.pageNum - 1 : 0;
				this.showPage(page);
				this.updatePageBar();
				this.scrollToPage(page);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.pageBarNext, 'click', function(e) {
				var page = (this.pageNum >= this.pages - 1) ? this.pages - 1 : this.pageNum + 1;
				this.showPage(page);
				this.updatePageBar();
				this.scrollToPage(page);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.pageBarLast, 'click', function(e) {
				var page = this.pages - 1;
				this.showPage(page);
				this.updatePageBar();
				this.scrollToPage(page);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.pageBar, 'mousemove', function(e) {
				this.fx.scroll.halt();
				var xPos = e.coords[0] - ub.n.cumulativeOffset(this.pageBar)[0];
				var hidden = this.pageBar.scrollWidth - this.pageBar.clientWidth;
				this.pageBar.scrollLeft = (xPos / this.pageBar.clientWidth) * hidden;
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.pageBar, 'mouseout', function(e) {
				if(e.relatedTarget != this.pageBar && !ub.n.contains(this.pageBar, e.relatedTarget))
					this.scrollToPage(this.pageNum);
			}, false, this));
			if(this.options.downloadUri) {
				ub.a.append(this.events, ub.e.attach(this.pageBarSave, 'click', function(e) {
					window.location = this.options.downloadUri;
				}, false, this));
			}
			this.setRecords(this.options.records);
		};
		dataGrid.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.container = null;
			this.pageBarContainer = null;
			this.pageBarLeft = null;
			this.pageBarFirst = null;
			this.pageBarPrev = null;
			this.pageBarNext = null;
			this.pageBarLast = null;
			this.pageBar = null;
			this.pageBarRight = null;
			this.table = null;
			this.thead = null;
			this.tfoot = null;
			this.tbody = null;
			for(var p in this.fx) {
				if(this.fx[p] && this.fx[p].destruct) {
					this.fx[p].destruct();
					 this.fx[p] = null;
				}
			}
			this.delRecords();
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			}, this, false);
			ub.a.clear(this.events);
			this.destruct = function() {};
		};
		dataGrid.prototype.setRecords = function(records, field, descending) {
			field = (!field) ? this.headers[0] : field;
			descending = (!descending) ? false : true;
			this.delRecords();
			this.records = records;
			this.buildTable();
			this.sortRecords(records, field, descending);
			this.updatePageBar();
			this.updateHeader();
			this.showPage(0);
		};
		dataGrid.prototype.delRecords = function() {
			//remove all extendos
			ub.a.each(this.records, function(record) {
				if(record.tr) {
					record.tr = null;
				}
			}, this, true);
			//remove all record events
			ub.a.each(this.recordEvents, function(event) {
				if(event.detach) event.detach();
			}, this, false);
			ub.a.clear(this.recordEvents);
		};
		dataGrid.prototype.buildTable = function() {
			var records = (this.records.length) ? this.records : [{ 'No records': 'No records present' }];
			
			ub.a.clear(this.headers);
			for(var p in records[0]) {
				//skip any user prototyped functions
				if(typeof(records[0][p]) == 'function')
					continue;
				ub.a.append(this.headers, p);
			}
			ub.n.removeChildren(this.pageBar);
			this.pages = Math.ceil(records.length/this.options.pageLength);
			var span = document.createElement('span');
			span.className = 'ubDataGridPage';
			//span.innerHTML = '&nbsp;';
			span.innerHTML = ' ';
			this.pageBar.appendChild(span);
			ub.u.repeat(function(i) {
				var span = document.createElement('span');
				span.className = 'ubDataGridPage';
				span.innerHTML = i + 1;
				this.pageBar.appendChild(span);
				ub.a.append(this.recordEvents, ub.e.attach(span, 'click', function(e) {
					this.showPage(i);
					this.updatePageBar();
				}, false, this));
				span = null;
			}, this.pages, this);
			var span = document.createElement('span');
			span.className = 'ubDataGridPage';
			//span.innerHTML = '&nbsp;';
			span.innerHTML = ' ';
			this.pageBar.appendChild(span);
			
			//build the table head
			ub.n.removeChildren(this.thead);
			var tr = document.createElement('tr');
			tr.className = 'ubDataGridTheadTr';
			this.thead.appendChild(tr);
			ub.a.each(this.headers, function(header) {
				var th = document.createElement('th');
				th.className = 'ubDataGridTheadTh';
				th.innerHTML = header;
				tr.appendChild(th);
				//click handler for th elements
				ub.a.append(this.recordEvents, ub.e.attach(th, 'click', function(e) {
					this.sortRecords(records, header, (this.sortField == header) ? !this.descending : this.descending);
					this.updateHeader();
					this.showPage(this.pageNum);
					return false;
				}, false, this));
				th = null;
			}, this);
			tr = null;
			//build the record tr's
			ub.a.each(records, function(record) {
				var tr = document.createElement('tr');
				tr.className = 'ubDataGridTbodyTr';
				//extendo
				tr.record = record;
				//extendo
				record.tr = tr;
				for(var p in record) {
					//skip any user prototyped functions or our extendo
					if(typeof(record[p]) == 'function' || p == 'tr')
						continue;
					var td = document.createElement('td');
					td.className = 'ubDataGridTbodyTd';
					tr.appendChild(td);
					td.innerHTML = (typeof(record[p]) == 'object') ? record[p].display : record[p];
					td = null;
				}
				tr = null;
			}, this);
			
			this.updatePageBar();
			this.updateHeader();
		};
		dataGrid.prototype.sortRecords = function(records, field, descending) {
			this.descending = descending;
			this.sortField = field;
			records.sort(function(a, b) {
				//fetch the values of a and b
				var aVal = (typeof(a[field]) == 'object') ? a[field]['value'] : a[field];
				var bVal = (typeof(b[field]) == 'object') ? b[field]['value'] : b[field];
				var diff = 0;
				var forceAlphaSort = (typeof(a[field]) == 'object') ? a[field]['forceAlphaSort'] : false;
				//sniff the value type
				if(!forceAlphaSort && ub.u.isFloat(aVal)) {
					aVal = parseFloat(aVal);
					bVal = parseFloat(bVal);
				}
				diff = (bVal > aVal) ? 1 : ((bVal < aVal) ? -1 : 0);
				//flip the difference if descending argument was passed
				return (descending) ? diff : -diff;
			});
		};
		dataGrid.prototype.showPage = function(pageNum) {
			this.pageNum = pageNum;
			ub.n.removeChildren(this.tbody);
			for(var i=this.options.pageLength * pageNum;i<this.options.pageLength * pageNum + this.options.pageLength;i++) {
				if(this.records[i] && this.records[i].tr)
					this.tbody.appendChild(this.records[i].tr);
			}
		};
		dataGrid.prototype.updateHeader = function() {
			var tds = this.thead.getElementsByTagName('th');
			ub.a.each(tds, function(td, i) {
				ub.n.removeClassName(td, 'ubDataGridSortAsc');
				ub.n.removeClassName(td, 'ubDataGridSortDesc');
				if(td.innerHTML == this.sortField) {
					if(this.descending) {
						ub.n.addClassName(td, 'ubDataGridSortDesc');
					}
					else {
						ub.n.addClassName(td, 'ubDataGridSortAsc');
					}
				}
			}, this);
		};
		dataGrid.prototype.scrollToPage = function(pageNum) {
			var spans = this.pageBar.getElementsByTagName('span');
			for(var i=0;i<spans.length;i++) {
				if(parseInt(spans[i].innerHTML) - 1 == pageNum) {
					this.fx.scroll.halt();
					var spanLeft = ub.n.cumulativeOffset(spans[i])[0];
					var pageBarLeft = ub.n.cumulativeOffset(this.pageBar)[0];
					var pageBarWidth = this.pageBar.clientWidth;
					var spanWidth = ub.n.cumulativeOffset(spans[i + 1])[0] - ub.n.cumulativeOffset(spans[i])[0];
					var pageBarScrolled = this.pageBar.scrollLeft;
					var spanOffset = spanLeft - pageBarLeft;
					if(spanOffset < pageBarScrolled) {
						this.fx.scroll.move([spanOffset,0]);
					}
					else if(spanOffset + spanWidth > pageBarScrolled + pageBarWidth) {
						this.fx.scroll.move([
							pageBarScrolled - (
								(pageBarScrolled + pageBarWidth) - 
								(spanOffset + spanWidth)
							),0]);
					}
					break;
				}
			}
		};
		dataGrid.prototype.updatePageBar = function() {
			var spans = this.pageBar.getElementsByTagName('span');
			ub.a.each(spans, function(span, i) {
				ub.n.removeClassName(span, 'ubDataGridPageActive');
				if(parseInt(span.innerHTML) - 1 == this.pageNum) {
					ub.n.addClassName(span, 'ubDataGridPageActive');
				}
			}, this);
		};
		return dataGrid;
	}(),
	gallery: function() {
		var gallery = function(options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				images: [],
				thumbs: [],
				preCache: false
			}, options || {});
			this.events = [];
			this.fx = {};
			this.cache = [];
			this.images = [];
			this.imageEvents = [];
			this.imageFx = [];
			this.curImage = 0;
			
			this.container = document.createElement('div');
			this.container.className = 'ubGalleryContainer';
			
			this.viewer = document.createElement('div');
			this.viewer.className = 'ubGalleryViewer';
			this.container.appendChild(this.viewer);
			
			this.loading = document.createElement('div');
			this.loading.className = 'ubGalleryLoading';
			this.container.appendChild(this.loading);
			
			this.prior = document.createElement('div');
			this.prior.className = 'ubGalleryPrior';
			this.container.appendChild(this.prior);
			
			this.next = document.createElement('div');
			this.next.className = 'ubGalleryNext';
			this.container.appendChild(this.next);
			
			this.strip = document.createElement('div');
			this.strip.className = 'ubGalleryStrip';
			this.container.appendChild(this.strip);
			
			this.stripTable = document.createElement('table');
			this.stripTable.className = 'ubGalleryStripTable';
			this.strip.appendChild(this.stripTable);
			
			this.stripTbody = document.createElement('tbody');
			this.stripTbody.className = 'ubGalleryStripTbody';
			this.stripTable.appendChild(this.stripTbody);
			
			this.stripTr = document.createElement('tr');
			this.stripTr.className = 'ubGalleryStripTr';
			this.stripTbody.appendChild(this.stripTr);
			
			this.fx.stripScroller = new ub.fx.scroll(this.strip);
			this.fx.loadingOpacity = new ub.fx.opacity(this.loading, { maxOpacity: 0.75 });
			this.fx.priorOpacity = new ub.fx.opacity(this.prior, { minOpacity: 0.25, maxOpacity: 0.7 });
			this.fx.nextOpacity = new ub.fx.opacity(this.next, { minOpacity: 0.25, maxOpacity: 0.7 });
			
			this.fx.loadingOpacity.hide();
			this.fx.priorOpacity.hide();
			this.fx.nextOpacity.hide();
			
			ub.a.append(this.events, ub.e.attach(this.viewer, 'mousemove', function(e) {
				this.scrollImage(e.coords);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.prior, 'mousemove', function(e) {
				this.scrollImage(e.coords);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.next, 'mousemove', function(e) {
				this.scrollImage(e.coords);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.prior, 'mouseover', function(e) {
				if(e.relatedTarget != this.prior && !ub.n.contains(this.prior, e.relatedTarget)) {
					this.fx.priorOpacity.halt();
					this.fx.priorOpacity.expose();
				}
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.prior, 'mouseout', function(e) {
				if(e.relatedTarget != this.prior && !ub.n.contains(this.prior, e.relatedTarget)) {
					this.fx.priorOpacity.halt();
					this.fx.priorOpacity.conceal();
				}
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.next, 'mouseover', function(e) {
				if(e.relatedTarget != this.next && !ub.n.contains(this.next, e.relatedTarget)) {
					this.fx.nextOpacity.halt();
					this.fx.nextOpacity.expose();
				}
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.next, 'mouseout', function(e) {
				if(e.relatedTarget != this.next && !ub.n.contains(this.next, e.relatedTarget)) {
					this.fx.nextOpacity.halt();
					this.fx.nextOpacity.conceal();
				}
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.prior, 'click', function(e) {
				if(this.curImage == 0) return;
				this.showImage(this.curImage - 1);
				this.scrollToThumb(this.curImage);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.next, 'click', function(e) {
				if(this.curImage == this.images.length - 1) return;
				this.showImage(this.curImage + 1);
				this.scrollToThumb(this.curImage);
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.strip, 'mousemove', function(e) {
				var xPos = e.coords[0] - ub.n.cumulativeOffset(this.strip)[0];
				var hidden = this.strip.scrollWidth - this.strip.clientWidth;
				this.strip.scrollLeft = (xPos / this.strip.clientWidth) * hidden;
			}, false, this));
			ub.a.append(this.events, ub.e.attach(this.strip, 'mouseout', function(e) {
				if(e.relatedTarget != this.strip && !ub.n.contains(this.strip, e.relatedTarget))
					this.scrollToThumb(this.curImage);
			}, false, this));
			
			if(this.options.images.length)
				this.setImages(this.options.images, this.options.thumbs);
		};
		gallery.prototype.destruct = function() {
			this.removeImages();
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			}, this, false);
			ub.a.clear(this.events);
			for(var p in this.fx) {
				if(typeof(this.fx[p]) == 'object' && this.fx[p].destruct) {
					this.fx[p].destruct();
					this.fx[p] = null;
				}
			}
			this.container = null;
			this.viewer = null;
			this.loading = null;
			this.prior = null;
			this.next = null;
			this.strip = null;
			this.stripTable = null;
			this.stripTbody = null;
			this.stripTr = null;
			this.destruct = function() {};
		};
		gallery.prototype.removeImages = function() {
			ub.n.removeChildren(this.stripTr);
			ub.a.each(this.imageEvents, function(event) {
				if(event.detach)
					event.detach();
			}, this);
			ub.a.clear(this.imageEvents);
			ub.a.each(this.imageFx, function(fx) {
				if(fx.destruct)
					fx.destruct();
			}, this);
			ub.a.clear(this.imageFx);
			ub.a.clear(this.images);
			ub.a.each(this.cache, function(cached) {
				cached.img = null;
			}, this);
			ub.a.clear(this.cache);
		};
		gallery.prototype.setImages = function(images, thumbs) {
			this.removeImages();
			this.images = images;
			this.stripTr.appendChild(document.createElement('td'));
			ub.a.each(images, function(image, i) {
				var td = document.createElement('td');
				td.className = 'ubGalleryStripTd';
				this.stripTr.appendChild(td);
				
				var outer = document.createElement('span');
				outer.className = 'ubGalleryThumbOuter';
				td.appendChild(outer);
				
				var inner = document.createElement('div');
				inner.className = 'ubGalleryThumbInner';
				outer.appendChild(inner);
				
				var thumb = document.createElement('img');
				thumb.className = 'ubGalleryThumb';
				if(thumbs[i]) {
					thumb.src = thumbs[i];
					if(this.options.preCache) {
						var img = document.createElement('img');
						img._load = ub.e.attach(img, 'load', function(e) {
							if(!this.fetchCached(image))
								ub.a.append(this.cache, { src: image, img: img });
							img._load.detach();
							img._load = null;
						}, false, this);
						img.src = image;
					}
				}
				else {
					thumb.src = image;
				}
				inner.appendChild(thumb);
				
				ub.a.append(this.imageFx, new ub.fx.opacity(thumb, { minOpacity: 0.5, maxOpacity: 1 }));
				this.imageFx[i].hide();
				ub.a.append(this.imageEvents, ub.e.attach(thumb, 'click', function(e) {
					this.showImage(i);
				}, false, this));
				ub.a.append(this.imageEvents, ub.e.attach(thumb, 'mouseover', function(e) {
					this.imageFx[i].halt();
					this.imageFx[i].expose();
				}, false, this));
				ub.a.append(this.imageEvents, ub.e.attach(thumb, 'mouseout', function(e) {
					if(i == this.curImage) return;
					this.imageFx[i].halt();
					this.imageFx[i].conceal();
				}, false, this));
				
				td = null;
				outer = null;
				inner = null;
				thumb = null;
			}, this);
			this.stripTr.appendChild(document.createElement('td'));
			if(images.length)
				this.showImage(0);
		};
		gallery.prototype.fetchCached = function(image) {
			for(var i=0;i<this.cache.length;i++) {
				if(this.cache[i].src == image)
					return this.cache[i].img;
			}
			return false;
		};
		gallery.prototype.showImage = function(i) {
			this.imageFx[this.curImage].halt();
			this.imageFx[this.curImage].conceal();
			this.curImage = i;
			this.imageFx[i].halt();
			this.imageFx[i].expose();
			var cached = this.fetchCached(this.images[i]);
			if(!cached) {
				var img = document.createElement('img');
				img.className = 'ubGalleryImageLarge';
				img._load = ub.e.attach(img, 'load', function(e) {
					ub.a.append(this.cache, { src: this.images[i], img: img });
					ub.n.removeChildren(this.viewer);
					this.fx.loadingOpacity.halt();
					this.fx.loadingOpacity.conceal();
					img._load.detach();
					img._load = null;
					this.showImage(i);
				}, false, this);
				this.fx.loadingOpacity.expose();
				ub.u.setTimeout(function() {
					img.src = this.images[i];
				}, this.fx.loadingOpacity.options.duration + 10, this);
			}
			else {
				ub.n.removeChildren(this.viewer);
				this.viewer.appendChild(cached);
				this.viewer.scrollTop = 0;
				this.viewer.scrollLeft = 0;
			}
		};
		gallery.prototype.scrollImage = function(coords) {
			var viewerOffset = ub.n.cumulativeOffset(this.viewer);
			var xPos = coords[0] - viewerOffset[0];
			var yPos = coords[1] - viewerOffset[1];
			var xHidden = this.viewer.scrollWidth - this.viewer.clientWidth;
			var yHidden = this.viewer.scrollHeight - this.viewer.clientHeight;
			this.viewer.scrollTop = (yPos / this.viewer.clientHeight) * yHidden;
			this.viewer.scrollLeft = (xPos / this.viewer.clientWidth) * xHidden;
		};
		gallery.prototype.scrollToThumb = function(i) {
			var tds = this.stripTr.getElementsByTagName('td');
			for(var j=0;j<tds.length;j++) {
				if(j == i + 1) {
					this.fx.stripScroller.halt();
					var imgLeft = ub.n.cumulativeOffset(tds[j])[0];
					var stripLeft = ub.n.cumulativeOffset(this.strip)[0];
					var stripWidth = this.strip.clientWidth;
					var imgWidth = ub.n.cumulativeOffset(tds[j + 1])[0] - ub.n.cumulativeOffset(tds[j])[0];
					var stripScrolled = this.strip.scrollLeft;
					var imgOffset = imgLeft - stripLeft;
					if(imgOffset < stripScrolled) {
						this.fx.stripScroller.move([imgOffset,0]);
					}
					else if(imgOffset + imgWidth > stripScrolled + stripWidth) {
						this.fx.stripScroller.move([
							stripScrolled - (
								(stripScrolled + stripWidth) - 
								(imgOffset + imgWidth)
							),0]);
					}
					break;
				}
			}
		};
		return gallery;
	}(),
	select: function() {
		var select = function(node, options) {
			ub.a.append(ub.widget.widgets, this);
			this.options = ub.u.extend({
				id: null,
				choices: []
			}, options || {});
			this.node = node;
			this.events = [];
			this.choices = [];
			this.container = document.createElement('div');
			this.containerInner = document.createElement('div');
			this.fold = new ub.widget.fold();
			this.fold.stretcher.height.hide();
			
			ub.n.addClassName(this.container, 'ubSelectContainer');
			ub.n.addClassName(this.containerInner, 'ubSelectContainerInner');
			ub.n.addClassName(this.fold.stretcher.stretcher, 'ubSelectMenuStretcher');
			ub.n.addClassName(this.fold.stretcher.container, 'ubSelectMenuContainer');
			
			ub.a.append(this.events, ub.e.attach(this.container, 'mouseout', function(e) {
				if(!ub.n.contains(this.container, e.relatedTarget)) {
					this.fold.stretcher.height.halt();
					this.fold.stretcher.height.conceal();
				}
			}, false, this));
			
			//add any choices
			ub.a.each(this.options.choices, function(choice) {
				if(ub.u.isObject(choice)) {
					this.createChoice(choice.display, choice.value);
				}
				else if(ub.u.isString(choice)) {
					this.createChoice(choice);
				}
			}, this);
			
			this.container.appendChild(this.containerInner);
			this.containerInner.appendChild(this.fold.container);
			
			this.replaceSelect(this.node);
			this.setChoice(this.choices[0]);
		};
		select.prototype.destruct = function() {
			ub.a.remove(ub.widget.widgets, this);
			this.node = null;
			ub.a.each(this.events, function(event) {
				if(event.detach) event.detach();
			}, this, false);
			ub.a.clear(this.events);
			this.container = null;
			this.fold.destruct();
			this.destruct = function() {};
			for(var i=this.choices.length-1;i>=0;i--)
				this.destroyChoice(this.choices[i]);
		};
		select.prototype.replaceSelect = function(node) {
			if(node.nodeName.toLowerCase() != 'select') return;
			var options = node.getElementsByTagName('option');
			ub.a.each(options, function(option) {
				var display = option.innerHTML;
				var value = (option.getAttribute('value')) ? option.getAttribute('value') : display;
				this.createChoice(display, value);
			}, this);
			if(node.parentNode) {
				node.parentNode.insertBefore(this.container, node);
			}
			var dims = ub.n.getDims(node);
			ub.n.setDims(this.container, dims);
			this.containerInner.style['width'] = dims[0] +'px';
			node.style['display'] = 'none';
		};
		select.prototype.setChoice = function(choice) {
			ub.n.removeChildren(this.fold.bar.title);
			this.fold.bar.title.appendChild(document.createTextNode(choice.display));
			this.node.selectedIndex = ub.a.indexOf(this.choices, choice);
			this.onsetchoice(new ub.e.event('setchoice'), choice);
		};
		select.prototype.createChoice = function(display, value) {
			var choice = ub.a.append(this.choices, {
				display: display,
				value: value
			});
			choice.node = document.createElement('div');
			ub.n.addClassName(choice.node, 'ubSelectChoice');
			choice.node.appendChild(document.createTextNode(display));
			
			choice.eClick = ub.e.attach(choice.node, 'click', function(e) {
				this.setChoice(choice);
				this.fold.stretcher.height.conceal();
			}, false, this);
			this.updateChoices();
			return choice;
		};
		select.prototype.destroyChoice = function(choice) {
			ub.a.remove(this.choices, choice);
			choice.node = null;
			choice.display = null;
			choice.value = null;
			choice.eClick.detach();
			choice.eClick = null;
		};
		select.prototype.updateChoices = function() {
			ub.n.removeChildren(this.fold.stretcher.container);
			ub.a.each(this.choices, function(choice) {
				this.fold.stretcher.container.appendChild(choice.node);
			}, this);
		};
		select.prototype.onsetchoice = function(e, choice) {};
		return select;
	}()
}).init();

