/*
 * Glow JavaScript Library
 * Copyright (c) 2008 British Broadcasting Corporation
 */
/*@cc_on @*//*@if (@_jscript_version > 5.1)@*/;/*
Namespace: glow.widgets

The glow.widgets module contains generic functionality used by our widgets, but currently no public API. To use our widgets, see the widget APIs:

  - <Mask>
  - <Overlay>
  - <Panel>

*/
glow.module("glow.widgets", "0.4.0", {
	require: [
		'glow.dom',
		'glow.events'
	],
	implementation: function() {
		var doc,
			docBody,
			env = glow.env;
		
		glow.ready(function() {
			doc = document;
			docBody = doc.body;
			
			//check if css or images are disabled, add class name "glow-basic" to body if they aren't
			var testDiv = glow.dom.create('<div class="glow-cssTest"></div>').appendTo(docBody);
			if (testDiv.css("z-index") != "1234" || testDiv.css("background-image").indexOf("ctr.png") == -1) {
				docBody.className += " glow-basic";
			}
			testDiv.remove();
			//add some IE class names to the body to help widget styling
			env.ie && (docBody.className += " glow-ie");
			//note: we apply the class "glow-ielt7" for IE7 if it's in quirks mode
			(env.ie < 7 || !env.standardsMode) && (docBody.className += " glow-ielt7");
			//some rounding issues in firefox when using opacity, so need to have a workaround
			env.gecko && (docBody.className += " glow-gecko");
			
		});
		
		
		return {
			/*
			PrivateMethod: _scrollPos
				Get the scroll position of the document. Candidate for moving into glow.dom?
			*/
			_scrollPos: function() {
				var win = window,
					docElm = env.standardsMode ? doc.documentElement : docBody;
				
				return {
					x: docElm.scrollLeft || win.pageXOffset || 0,
					y: docElm.scrollTop || win.pageYOffset || 0
				};
			}
		};
	}
});
;glow.module("glow.widgets.Mask", "0.4.0", {
	require: [
		'glow.dom',
		'glow.events',
		'glow.widgets'
	],
	implementation: function() {
		var dom = glow.dom,
			$ = dom.get,
			events = glow.events,
			widgets = glow.widgets,
			bodyProperties, //this is a holding place for body padding & margins
			htmlStr = '<div class="glow-noMask" style="margin:0;padding:0;position:absolute;width:100%;top:0;left:0;overflow:auto;', //reusable html string
			noScrollContainer,
			iframeSrc = '<iframe class="glow-noMask" style="margin:0;padding:0;position:absolute;top:0;left:0;filter:alpha(opacity=0);display:none"></iframe>';
		
		/*
		Class: glow.widgets.Mask
			A mask provides a background for a modal dialog, overlay, lightbox, etc. Use
			this if you're wanting to block out the main content of the page. Anything you
			want to be on top of the mask needs to have a higher z-index (default: 9990).
		
		Constructor:
			(code)
			var myMask = new glow.widgets.Mask();
			(end)
		
		Arguments:
			opts - (optional object) object containing the attributes specified below.

		opts:
		
			The options can contain zero or more of the following
		
			opacity - (number from 0 to 1, default 0.7) the opacity of the layer that indicates
					  that the background content is inactive.
					  
			color - (css colour, default black) the colour of the layer that indicate that the background
				   content is inactive.
				   
			onClick - (function, default undefined) shortcut to attach an event listener
					  that is called when the user clicks on the background.
					  
			zIndex - (default 9990) the z-index of the opaque layer. allowScroll is not set
					 then the content of the page will be put in a div with a z-index one less than
					 the value of this attribute.
					 
			disableScroll - (boolean, default false) If set to true, scrolling is disabled
						    in the main document. This feature is experimental. It works by moving
							the document into a new container, offsetting it and setting overflow
							to none. Because this adds a new element between body and your document,
							you may have problems if your scripts rely on certain elements. Children of <body>
							which have class "glow-noMask" will be left as children of <body>

		Example:
			(code)
			var mask = new glow.widget.Mask({
				onClick : function () {
					this.remove();
				}
			});
			mask.add();
			(end)
		*/
		function Mask(opts) {
			this.opts = glow.lang.apply({
				color: '#000',
				opacity: 0.7,
				zIndex: 9900,
				disableScroll: false
			}, opts || {});
			
			/*
			Property: maskElement
				The node overlayed to create the mask. Access this if you want to
				change its properties on the fly
			*/
			var docBody = document.body,
				mask = this.maskElement = dom.create(
				htmlStr + 'z-index:' + this.opts.zIndex + ';background:' + this.opts.color + ';visibility:hidden"></div>'
			).appendTo(docBody),
				that = this;
			
			mask.css("opacity", this.opts.opacity);
			
			if (glow.env.ie < 7) {
				this._iframe = dom.create(iframeSrc).css("z-index", this.opts.zIndex - 1).appendTo(docBody);
			}
			
			//add mask node click event, route it through to a Mask event
			events.addListener(mask, "click", function() {
				events.fire(that, "click");
			});
			if (this.opts.onClick) {
				events.addListener(this, "click", opts.onClick);
			}
		}

		Mask.prototype = {
			/*
			Method: add
				Displays the mask.
			*/
			add: function () {
				var doc = $(document),
					body = $(document.body),
					win = $(window),
					that = this;
			
				if (this.opts.disableScroll && !noScrollContainer) { //avoid blocking scrolling twice
					noScrollContainer = glow.dom.create(
						htmlStr + 'height:100%;overflow:hidden;">' + htmlStr + '"></div></div>'
					);
					
					var scrollVals = widgets._scrollPos(),
						bodyStyle = body[0].style,
						clientHeight = win.height(),
						clientWidth = win.width(),
						noScroll = noScrollContainer.get("div"),
						//get children which don't have class "glow-noMask"
						bodyChildren = body.children().filter(function() { return (' ' + this.className + ' ').indexOf("glow-noMask") == -1 });
					
					bodyProperties = {
						margin: [body.css("margin-top"), body.css("margin-right"), body.css("margin-bottom"), body.css("margin-left")],
						padding: [body.css("padding-top"), body.css("padding-right"), body.css("padding-bottom"), body.css("padding-left")],
						height: body.css("height")
					};
					
					bodyStyle.margin = bodyStyle.padding = 0;
					
					bodyStyle.height = "100%";
					noScroll[0].style.zIndex = this.opts.zIndex - 1;
					
					noScrollContainer.appendTo(body);
					
					noScroll.css("margin", bodyProperties.margin.join(" ")).
							 css("padding", bodyProperties.padding.join(" ")).
							 css("top", -scrollVals.y - parseFloat(bodyProperties.margin[0]) + "px").
							 css("left", -scrollVals.x + "px").
							 append(bodyChildren);
				}
				
				function resizeMask() {
					var bodyHeight = body.height();
					for (var i = 0; i < 2; i++) {
						that.maskElement.css("width", "100%").
								  css("height", (that.opts.disableScroll ? noScrollContainer.height() : Math.max(bodyHeight, win.height())) + "px");
					}
					if (glow.env.ie < 7) {
						var maskStyle = that.maskElement[0].style;
						that._iframe.css("width", maskStyle.width).css("height", maskStyle.height);
					}
				}
				this.maskElement.css("visibility", "visible").css("display", "block");
				if (glow.env.ie < 7) {
					this._iframe.css("display", "block");
				}
				resizeMask();
				this._resizeListener = events.addListener(window, "resize", resizeMask);
			},

			/*
			Method: remove
				Removes the mask.
			*/
			remove : function () {
				this.maskElement.css("visibility", "hidden").css("display", "none");
				if (glow.env.ie < 7) {
					this._iframe.css("display", "none");
				}
				events.removeListener(this._resizeListener);
				
				if (this.opts.disableScroll) {
					var body = $(document.body),
						noScroll = noScrollContainer.children();
					
					noScroll.children().appendTo(body);
					window.scroll(-parseInt(noScroll.css("left")), -parseInt(noScroll.css("top")));
					noScrollContainer.remove();
					body.css("margin", bodyProperties.margin.join(" ")).
						 css("padding", bodyProperties.padding.join(" ")).
						 css("height", bodyProperties.height);
						 
					delete noScrollContainer;
					noScrollContainer = undefined;
				}
				
			}
		};

		return Mask;
	}
});
;glow.module("glow.widgets.Overlay", "0.4.0", {
	require: [
		'glow.dom',
		'glow.events',
		'glow.anim',
		'glow.widgets',
		'glow.widgets.Mask'
	],
	implementation: function() {
		var dom = glow.dom,
			$ = dom.get,
			events = glow.events,
			widgets = glow.widgets,
			env = glow.env,
			anim = glow.anim,
			tweens = glow.tweens,
			overlayHtml = '<div class="glow-overlay glow-noMask"></div>',
			//this iframe code is duplicated in mask... shall we sort that out?
			iframeSrc = '<iframe class="glow-noMask" style="display:none;margin:0;padding:0;position:absolute;filter:alpha(opacity=0)"></iframe>',
			hiddenFlash = [],
			flashUrlTest = /.swf($|\?)/i,
			wmodeTest = /<param\s+(?:[^>]*(?:name=["'?]\bwmode["'?][\s\/>]|\bvalue=["'?](?:opaque|transparent)["'?][\s\/>])[^>]*){2}/i;
		
		
		/*
		PrivateMethod: hideWindowedFlash
			Goes through flash embeds on a page and hides them if they're windowed.
			Otherwise, they'll appear above the overlay.
		*/	
		function hideWindowedFlash(overlay) {
			//return if they've already been hidden, saves time
			if (hiddenFlash.length) { return; }
			var i = 0;
			
			$("object, embed").each(function() {
				var that = this, wmode;
				//we need to use getAttribute here because Opera & Safari don't copy the data to properties
				if (
					(that.getAttribute("type") == "application/x-shockwave-flash" ||
					flashUrlTest.test(that.getAttribute("data") || that.getAttribute("src") || "") ||
					(that.getAttribute("classid") || "").toLowerCase() == "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000") &&
					!$(that).isWithin(overlay.content)
				) {
					wmode = that.getAttribute("wmode");
					if (
						(that.nodeName == "OBJECT" && !wmodeTest.test(that.innerHTML)) ||
						(wmode != "transparent" && wmode != "opaque")
					) {
						hiddenFlash[i++] = [that, that.style.visibility];
						that.style.visibility = "hidden";
					}
					
				}
			});
			
		}
		
		/*
		PrivateMethod: revertWindowedFlash
			Reverses the actions of hideWindowedFlash
		*/	
		function revertWindowedFlash() {
			for (var i = 0, len = hiddenFlash.length; i < len; i++) {
				hiddenFlash[i][0].style.visibility = hiddenFlash[i][1];
			}
			hiddenFlash = [];
		}
		
		/*
		PrivateMethod: generatePresetAnimation
			Generates an animation / timeline object from one of the presets
			
		Arguments:
			overlay - reference to the overlay
			preset - name of the preset animation
			show - true for 'show' animation. Otherwise 'hide'.
		*/
		function generatePresetAnimation(overlay, show) {
			var channels = [],
				channel = [],
				chanLen = 0,
				chansLen = 0,
				preset = overlay.opts.anim,
				mask = overlay.opts.mask,
				container = overlay.container,
				maskOpacity,
				finalHeight = 0;
			
			if (preset == "fade") {
				container.css("opacity", (show ? 0 : 1));
				channels[chansLen++] = [
					anim.css(container, 0.3, {
							opacity: {
								from: (show ? 0 : 1),
								to: (show ? 1 : 0)
							}
						}
					)
				];
				if (show) {
					channels[chansLen - 1][1] = function() { container.css("opacity", "") };
				}
				channels[chansLen++] = [generateMaskAnimation(overlay, show)];
			} else if (preset == "roll") {
				if (show) {
					container.css("height", "");
					finalHeight = container.height();
					container.css("height", "0");
				}
				
				channels[chansLen++] = [
					function() {
						/*
							safari doesn't properly recognise switches between 'hidden'
							and 'auto'
						*/
						if (env.webkit < 522 && show) {
							container.css("display", "none");
							setTimeout(function() {
								container.css("overflow", "hidden").css("display", "block");
							}, 0);
						} else {
							container.css("overflow", "hidden");
						}
					},
					anim.css(container, 0.3, {
						height: {to: finalHeight}
					}, {tween: show ? tweens.easeOut() : tweens.easeIn() }),
					function() {
						if (!show) {
							container.css("visibility", "hidden");
						}
						container.css("height", "");
						container.css("overflow", "");
					}
				];
				channels[chansLen++] = [generateMaskAnimation(overlay, show)];
			}
			return new anim.Timeline(channels);
		}
		
		/*
		PrivateMethod: generateMaskAnimation
			generates an animation for the mask of an overlay to go in a timeline
		*/
		function generateMaskAnimation(overlay, show) {
			if (! overlay.opts.modal) { return 0; }
		
			var mask = overlay.opts.mask,
				maskOpacity = mask.opts.opacity,
				maskElement = mask.maskElement;
			
			maskElement.css("opacity", (show ? 0 : maskOpacity));
			return anim.css(maskElement, 0.1, {
					opacity: {
						from: (show ? 0 : maskOpacity),
						to: (show ? maskOpacity : 0)
					}
				}
			)
		}
		
		/*
		PrivateMethod: closeOverlay
			Hides the overlay. Separated out so this part can happen asyncronously (like after an animation)
		*/
		function closeOverlay(overlay) {
			revertWindowedFlash();
			overlay.container.css("visibility", "").css("display", "");
			if (overlay.opts.modal) {
				overlay.opts.mask.remove();
			} else if (glow.env.ie < 7) {
				overlay._iframe.css("display", "none");
			}
			events.removeListener(overlay._scrollEvt);
			events.removeListener(overlay._resizeEvt);
		}
		
		/*
		Class: glow.widgets.Overlay
			A piece of content that sits on top of the other content on the page.

		Constructor:
			(code)
			var myOverlay = new glow.widgets.Overlay(content);
			(end)
			
		Arguments:
			content - (selector|DOMElement|NodeList) the element that contains the contents of the overlay. If this is
					   in the document it will be moved to document.body.
			opts - (optional object) object containing the attributes specified below.

		opts:
		
			The options can contain zero or more of the following
			
			modal - (bool, default false) is the overlay modal. If true then a default Mask will be created
					if one is not provided.
					
			mask - (glow.widget.Mask) used to indicate to the user that the overlay is modal. If provided
					then the modal property is set to true.
					
			closeOnMaskClick - (bool, default true) if true then listens for a click event on the mask
								and hides when it fires.
								
			anim - (string / object / function, default null). Specifies a transition for showing / hiding the panel. Can be "fade" or "roll",
					or a function which returns a <glow.anim.Animation> or <glow.anim.Timeline>. The function is passed the overlay
					as the first parameter, and 'true' if the overlay is showing, 'false' if it's hiding.
								
			zIndex - (number, default 9991) the z-index to set on the overlay. If the overlay is modal, the zIndex
					 of the mask will be set to one less than the value of this attribute.
					 
			autoPosition - (bool, default true) if true, the overlay will be positioned to the viewport according to the x & y
				options. If false, you will have to set the position manually by setting the left / right css styles of the
				container property.
					 
			x - (number / string, pixel or percentage) distance of overlay from the left of the viewport. If the unit is a percentage
				then 0% is aligned to the left of the viewport, 100% is aligned to the right of viewport and 50% is centered.
				
			y - (number / string, pixel or percentage) distance of overlay from the top of the viewport. If the unit is a percentage
				then 0% is aligned to the left of the viewport, 100% is aligned to the right of viewport and 50% is centered.

		Example:
			(code)
			var overlay = new glow.widgets.Overlay(
				glow.dom.create(
					'<div>' +
					'  <p>Your Story has been saved.</p>' +
					'</div>'
				)
			);
			overlay.show();
			(end)
		*/
		function Overlay(content, opts) {
			//assume modal if mask provided
			if (opts && opts.mask) { opts.modal = true; }
			
			this.opts = glow.lang.apply({
				modal: false,
				mask: new glow.widgets.Mask(opts.zIndex ? {zIndex: opts.zIndex-1} : {}),
				closeOnMaskClick: true,
				zIndex: 9990,
				autoPosition: true,
				x: "50%",
				y: "50%"
			}, opts || {});
			
			/*
			Property: content
				A <glow.dom.NodeList> containing the content of the overlay
			*/			
			var contentNode = this.content = $(content),
				that = this,
				/*
				Property: container
					A <glow.dom.NodeList> of the overlay's container. Use this to alter the width of the overlay. You can also
					manually position the overlay using this node when autoPosition is false.
				*/	
				overlayNode = this.container = dom.create(overlayHtml).css("z-index", this.opts.zIndex),
				docBody = document.body;
				
			/*
			Property: autoPosition
				Bool. If true, the overlay will be positioned to the viewport according to the x & y
				options. If false, you will have to set the position manually by setting the left / right css styles of the
				container property.
			*/
			this.autoPosition = this.opts.autoPosition;
			
			/*
			Property: isShown
				Bool. True if the overlay is showing.
			*/
			this.isShown = false;
			
			//this is used to prevent show / hide commands while animations are underway
			this._blockActions = false;
			
			//add the content to the page
			overlayNode.appendTo(docBody).append(contentNode);
			
			//add close event to mask if needed
			if (this.opts.closeOnMaskClick) {
				events.addListener(this.opts.mask, "click", function() {
					that.hide();
				});
			}
			
			//add IE iframe hack if needed
			if (glow.env.ie < 7 && !this.opts.modal) {
				this._iframe = dom.create(iframeSrc).css("z-index", this.opts.zIndex - 1).appendTo(docBody);
			}
		}
		
		Overlay.prototype = {
			/*
			Method: setPosition
				Change or recalculate the position of the overlay. Call with parameters to
				change the position of the overlay or call without parameters to recalculate
				the position of the overlay. You may need to call this without parameters
				if relative positions become invalid.
			
			Arguments:
				x - (number / string, pixel or percentage) distance of overlay from the left of the viewport. If the unit is a percentage
				then 0% is aligned to the left of the viewport, 100% is aligned to the right of viewport and 50% is centered.
				
				y - (number / string, pixel or percentage) distance of overlay from the top of the viewport. If the unit is a percentage
					then 0% is aligned to the left of the viewport, 100% is aligned to the right of viewport and 50% is centered.
					
				
			*/
			setPosition: function(x, y) {
				//don't use set position if autoPosition is false
				if (!this.autoPosition) {
					return this;
				}
				//if values have been provided, set them. Make sure we're not being passed an event object!
				if (x !== undefined && !(x.source)) {
					this.opts.x = x;
					this.opts.y = y;
				}
				var win = $(window),
					x = this.opts.x,
					y = this.opts.y,
					//fixed positioning isn't supported in IE6 (or quirks mode). Also, Safari 2 does some mental stuff with position:fixed so best to just avoid it
					useFixed = (!env.ie && !(env.webkit < 522)) || (env.ie > 6 && env.standardsMode),
					xVal = parseFloat(this.opts.x),
					yVal = parseFloat(this.opts.y),
					extraOffset = (this.opts.mask.opts.disableScroll || useFixed) ? {x:0,y:0} : widgets._scrollPos(),
					container = this.container;
				
				useFixed && container.css("position", "fixed");
				
				if (typeof x == "string" && x.indexOf("%") != -1) {
					container.css("left", Math.max(((win.width() - container[0].offsetWidth) * (xVal/100)) + extraOffset.x, extraOffset.x) + "px");
				} else {
					container.css("left", xVal + extraOffset.x + "px");
				}
				
				if (typeof y == "string" && y.indexOf("%") != -1) {
					container.css("top", Math.max(((win.height() - container[0].offsetHeight) * (yVal/100)) + extraOffset.y, extraOffset.y) + "px");
				} else {
					container.css("top", yVal + extraOffset.y + "px");
				}
				
				if (glow.env.ie < 7 && !this.opts.modal) {
					var overlayStyle = container[0].style;
					this._iframe.css("top", overlayStyle.top).
								 css("left", overlayStyle.left).
								 css("width", container[0].offsetWidth + "px").
								 css("height", container[0].offsetHeight + "px");
				}
				return this;
			},
			/*
			Method: show
				Displays the overlay.
			*/
			show: function() {
				var that = this,
					showAnim,
					animOpt = that.opts.anim;
				
				if (that._blockActions || that.isShown) { return that; }
				
				if (events.fire(that, "show").defaultPrevented()) {
					return that;
				}
				hideWindowedFlash(that);
				that.container.css("display", "block");
				if (that.opts.modal) {
					that.opts.mask.add();
				} else if (glow.env.ie < 7) {
					that._iframe.css("display", "block");
				}
				that._scrollEvt = events.addListener(window, "scroll", that.setPosition, that);
				that._resizeEvt = events.addListener(window, "resize", that.setPosition, that);
				
				that.setPosition();
				
				//run the appropiate animation
				if (typeof animOpt == "string") {
					showAnim = generatePresetAnimation(that, true);
				} else if (typeof animOpt == "function") {
					showAnim = animOpt(that, true);
				} else if (animOpt) {
					showAnim = animOpt.show;
				}
				if (showAnim) {
					if (! showAnim._overlayEvtAttached) {
						events.addListener(showAnim, "complete", function() {
							that._blockActions = false;
							that.isShown = true;
							events.fire(that, "afterShow");
						});
						showAnim._overlayEvtAttached = true;
					}
					that._blockActions = true;
					showAnim.start();
					that.container.css("visibility", "visible");
				} else {
					that.container.css("visibility", "visible");
					that.isShown = true;
					events.fire(that, "afterShow");
				}
				
				
				return that;
			},
			/*
			Method: hide
				Hides the overlay.
			*/
			hide: function() {			
				var that = this,
					hideAnim,
					animOpt = that.opts.anim;
				
				if (this._blockActions || !that.isShown) { return that; }
				
				if (events.fire(that, "hide").defaultPrevented()) {
					return that;
				}
				
				//run the appropiate animation
				if (typeof animOpt == "string") {
					hideAnim = generatePresetAnimation(that, false);
				} else if (typeof animOpt == "function") {
					hideAnim = animOpt(that, false);
				} else if (animOpt) {
					hideAnim = animOpt.hide;
				}
				if (hideAnim) {
					if (! hideAnim._overlayEvtAttached) {
						events.addListener(hideAnim, "complete", function() {
							closeOverlay(that);
							that._blockActions = false;
							that.isShown = false;
							events.fire(that, "afterHide");
						});
						hideAnim._overlayEvtAttached = true;
					}
					that._blockActions = true;
					hideAnim.start();
				} else {
					closeOverlay(that);
					that.isShown = false;
					events.fire(that, "afterHide");
				}
				return that;
			}
			/*
			Event: show
				Fired when the overlay is about to appear on the screen, before any animation. At this
				point you can access the content of the overlay and make changes before
				it is shown to the user. If you prevent the default action of this
				event (by returning false or calling event.preventDefault) the
				overlay will not show.
			
			Example:
				(code)
				var overlay = new glow.widgets.Overlay("#overlayContent");
				glow.events.addListener(overlay, "show", function() {
					//incerment a counter in the overlay by one
					var showCount = this.content.get("#showCount");
					showCount.text(parseInt(showCount.text()) + 1);
				});
				(end)
			*/
			/*
			Event: afterShow
				Fired when the overlay is visible to the user and any 'show' animation
				is complete. This event is ideal to assign focus to a particular part of
				the overlay. If you want to change content of the overlay before it
				appears, see the 'show' event.
			*/
			/*
			Event: hide
				Fired when the overlay is about to hide. If you prevent the default action of this
				event (by returning false or calling event.preventDefault) the
				overlay will not hide.
			*/
			/*
			Event: afterHide
				Fired when the overlay has fully hidden, after any hiding animation has
				completed.
			*/
		};
		
		return Overlay;
	}
});
;glow.module("glow.widgets.Panel", "0.4.0", {
	require: [
		'glow.dom',
		'glow.events',
		'glow.widgets.Overlay'
	],
	implementation: function() {
		var dom = glow.dom,
			$ = dom.get,
			events = glow.events,
			widgets = glow.widgets,
			Overlay = widgets.Overlay,
			lang = glow.lang,
			env = glow.env,
			defaultTemplate,
			//a hash of themes, true if their images have been preloaded
			themesPreloaded = {};
		
		/*
		PrivateMethod: generateDivString
			generates <div class="{arg}"></div> for first arg onwards if first arg is false.
			generates <div class="{arg}"><div></div></div> for first arg onwards if first arg is true.
		*/
		function generateDivString(nest) {
			var insideDiv = nest ? '<div></div>' : '';
			for (var i = 1, len = arguments.length, r = []; i < len; i++) {
				r[i-1] = '<div class="' + arguments[i] + '">' + insideDiv + '</div>';
			}
			return r.join("");
		}
		
		/*
		PrivateProperty: defaultTemplate
			default template for a panel & infoPanel
		*/
		defaultTemplate = function() {
			var r = [], rLen = 0;
			r[rLen++] = '<div class="glow-panel">';
				r[rLen++] = '<div class="glow-defaultSkin">';
					r[rLen++] = generateDivString(false, "glow-infoPanel-pointerT", "glow-infoPanel-pointerL", "glow-infoPanel-pointerR");
					r[rLen++] = '<div class="pc">';
						r[rLen++] = generateDivString(false, "tr", "tl");
						r[rLen++] = generateDivString(true, "tb");
						r[rLen++] = '<div class="tc">';
							r[rLen++] = generateDivString(false, "bars");
							r[rLen++] = '<div class="c">';
								r[rLen++] = '<a class="glow-panel-close" href="#" title="close">X</a>';
								r[rLen++] = generateDivString(false, "glow-panel-hd", "glow-panel-bd", "glow-panel-ft");
							r[rLen++] = '</div>';
						r[rLen++] = '</div>';
						r[rLen++] = generateDivString(false, "br", "bl");
						r[rLen++] = generateDivString(true, "bb");
					r[rLen++] = '</div>';
					r[rLen++] = generateDivString(false, "glow-infoPanel-pointerB");
				r[rLen++] = '</div>';
			r[rLen++] = '</div>';
			return r.join("");
		}();
		
		/*
		Class: glow.widgets.Panel
			A piece of content that sits on top of the other content on the page. This class extends
			<glow.widgets.Overlay> and adds a default style, close button and templating system.

		Constructor:
			(code)
			var myPanel = new glow.widgets.Panel(content);
			(end)
			
		Arguments:
			content - (selector|DOMElement|NodeList) the element that contains the contents of the Panel. If this is
					   in the document it will be moved to document.body. If your content node has a child element with class "hd"
					   it will be added to the header of the panel. Similarly, an element with class "ft" will be added to the
					   footer of the panel.
			opts - (optional object) object containing the attributes specified below.

		opts:
		
			The options can contain zero or more of the following
				
			width - (number / string, pixel or percentage) width of the panel. Default 400px, giving a content width of 360px in the default template
			
			theme - (string) The theme to use, only applies when using the default template. Currently support themes are "dark" and "light". Default is "dark"
			
			template - (string) an html template to use to create the panel.
			
			modal - (bool, default true) is the panel modal. If true then a default Mask will be created
					if one is not provided.
			
			From <glow.widgets.Overlay>:
					
			mask - (glow.widget.Mask) used to indicate to the user that the overlay is modal. If provided
					then the modal property is set to true.
					
			closeOnMaskClick - (bool, default true) if true then listens for a click event on the mask
								and hides when it fires.
								
			anim - (string / object / function, default null). Specifies a transition for showing / hiding the panel. Can be "fade" or "roll",
					or a function which returns a <glow.anim.Animation> or <glow.anim.Timeline>. The function is passed the overlay
					as the first parameter, and 'true' if the overlay is showing, 'false' if it's hiding.
								
			zIndex - (number, default 9991) the z-index to set on the overlay. If the overlay is modal, the zIndex
					 of the mask will be set to one less than the value of this attribute.
					 
			autoPosition - (bool, default true) if true, the overlay will be positioned to the viewport according to the x & y
				options. If false, you will have to set the position manually by setting the left / right css styles of the
				container property.
					 
			x - (number / string, pixel or percentage) distance of overlay from the left of the viewport. If the unit is a percentage
				then 0% is aligned to the left of the viewport, 100% is aligned to the right of viewport and 50% is centered.
				
			y - (number / string, pixel or percentage) distance of overlay from the top of the viewport. If the unit is a percentage
				then 0% is aligned to the left of the viewport, 100% is aligned to the right of viewport and 50% is centered.
			
		Templating:
		
			Your template must contain elements with these class names to identify the header, body and footer of your layout
			
			glow-panel-hd - The header of the panel
			
			glow-panel-bd - The body of the panel
			
			glow-panel-ft - The footer of the panel
			
			glow-panel-close - The close button of the panel (a click event will be added to this element)
		*/
		function Panel(content, opts) {
			content = $(content);
			opts = opts || {};
			
			if (typeof opts.width == "number") {
				opts.width += 'px';
			}
			
			if (opts.template) {
				var customTemplate = true;
			}
			
			opts = glow.lang.apply({
				template: defaultTemplate,
				width: "400px",
				modal: true,
				theme: "dark"
			}, opts);
			
			//dress content in template
			var fullContent = dom.create(opts.template),
				headContent = content.get("> .hd"),
				footerContent = content.get("> .ft"),
				docBody = document.body,
				that = this,
				fullContentClone;
				
			if (!customTemplate) {
				fullContent.addClass("glow-panel-" + opts.theme);
				//preload the images of the theme
				if (!themesPreloaded[opts.theme] && docBody.className.indexOf("glow-basic") == -1) {
					fullContentClone = fullContent.clone().addClass("glow-panel-preload").appendTo(docBody);
					themesPreloaded[opts.theme] = true;
				}
			}
			
			
			/*
				if we've been passed more than one node it's possible the user
				has ommited the container (usually if they're creating the panel
				from a string), let's be kind and deal with that.
			*/
			if (content.length > 1) {
				content.each(function() {
					var elm = $(this);
					if (elm.hasClass("hd")) {
						headContent = elm;
					} else if (elm.hasClass("ft")) {
						footerContent = elm;
					}
				});
			}
			
			/*
			Property: header
				A <glow.dom.NodeList> containing the panel's header element
			*/
			this.header = fullContent.get(".glow-panel-hd");
			/*
			Property: footer
				A <glow.dom.NodeList> containing the panel's footer element
			*/
			this.footer = fullContent.get(".glow-panel-ft");
			/*
			Property: body
				A <glow.dom.NodeList> containing the panel's body element
			*/
			this.body = fullContent.get(".glow-panel-bd");
			
			if (content.isWithin(docBody)) {
				fullContent.insertBefore(content);
			} else {
				fullContent.appendTo(docBody);
			}
			this.body.append(content);
			if (headContent.length) {
				this.header.append(headContent);
			} else if (!customTemplate) {
				fullContent.addClass("glow-panel-noHeader");
			}
			if (footerContent.length) { this.footer.append(footerContent); }
			
			events.addListener(fullContent.get(".glow-panel-close"), "click", function() {
				that.hide();
				return false;
			})
			
			Overlay.call(this, fullContent, opts);
			
			this.container.css("width", opts.width);
		}
		lang.extend(Panel, Overlay);
		
		return Panel;
	}
});
;
glow.module("glow.widgets.Sortable", "0.4.0", {
	require: [
		'glow.dom',
		'glow.events',
		'glow.dragdrop'
	],
	implementation: function() {

		var $ = glow.dom.get,
			events = glow.events,
			fire = events.fire,
			addListener = events.addListener;

		/*
		PrivateFunction: offsetTop

		Get the offsetTop of an element in a way that works in IE.

		Arguments:

			*element* (glow.dom.NodeList)

			The element to get the offsetTop of.

		Returns:
			An integer pixel offset.
		*/

		// TODO - this is a copy of the one in dragdrop, which should be moved to dom, removing the need for this
		function offsetTop (element) {
			var res = 0, el = element[0], pos;
			if (glow.env.ie) {
				do {
					res += el.offsetTop;
					el = el.offsetParent;
					if (el) pos = $(el).css('position');
				} while (el && ! (pos == 'absolute' || pos == 'fixed' || pos == 'relative'));
			}
			else {
				res = el.offsetTop;
			}
			return res;
		}

		/*
		Class: glow.widgets.Sortable

		Constructor:

			(code)
			new glow.widgets.Sortable(containers, opts)
			(end)

		Arguments:
			*containers* (glow.dom.NodeList/string/array of DOM Elements)

			The container or containers of the items to be made sortable.

			*opts* (object)

			A set of named options (see below).

		Options:
			*dropIndicatorClass* (string, default: glow-sortable-dropindicator)

			The name of the class to apply to the element that indicates where an item will be dropped.

			*equaliseColumns* (boolean, default true)
	
			Make the bottom of each container the same.

			*onSort* (function)

			Create an event listener that is fired when the sortable is sorted - i.e. after one of the
			draggables has been dragged.

		Group: Properties

		    Properties:

			    Property: containers (glow.dom.NodeList)

				The elements that contain the sortable items.

			    Property: draggables (array of glow.dragdrop.Draggables)

				Array of draggables that can be sorted. Read-only.

				Property: dropTargets (array of glow.dragdrop.DropTargets)

				Array of drop targets that draggable can be dragged to and sorted within. Read-only.

		Group: Events

		     Event: sort

			     Fired when an item in the sortable has been dragged.

		*/
		var Sortable = function (containers, opts) {
			this._opts = opts = glow.lang.apply({
				dropIndicatorClass : 'glow-sortable-dropindicator',
				equaliseColumns    : true,
				draggableOptions   : {}
			}, opts || {});

			this.constrainDragTo = opts.constrainDragTo;
			this.axis = opts.axis;
			this.draggables = [];

			var containers = this.containers = $(containers),
				dropTargets = this.dropTargets = [];

			if (opts.onSort)
				addListener(this, "sort", opts.onSort);

		    // drop targets
			containers.each(function (i) {
				dropTargets[i] = new glow.dragdrop.DropTarget(this, {
					tolerance          : 'intersect',
					dropIndicator      : 'spacer',
					dropIndicatorClass : opts.dropIndicatorClass
				});
			});

			// draggables
			this.addItems(containers.get("> *"));
		};


		/*
		Private method: equaliseColumns

		Sets the logical bottom of each drop target to the same position on the page. This
		allows sortable items to be dragged sideways into a column that does not extend
		as far as the column that the item is being dragged from.
		*/
		function equaliseColumns () {
		    var offsets = [], maxBottom = 0, bottom, dropTargets = this.dropTargets;
			this.containers.each(function (i) {
				var el = $(this);
				offsets[i] = offsetTop(el);
				bottom = offsets[i] + el[0].offsetHeight;
				if (glow.env.khtml) bottom -= el.css('margin-top') + el.css('margin-bottom');
				if (bottom > maxBottom) maxBottom = bottom;
			});
			for (var i = 0, l = this.dropTargets.length; i < l; i++)
				this.dropTargets[i].setLogicalBottom(maxBottom);
		}

		/*
		Private method: handleDrop

		Event handler that handles a draggable being dropped.
		*/
		function handleDrop (e) {
			var draggable = e.attachedTo,
				el = draggable.element,
				target = draggable.activeTarget;
		    this._previous = el.prev();
			this._parent = el.parent();
			if (target)	target.moveToPosition(draggable);
	    }

		/*
		Private method: handleAfterDrop

		Event handler that is called after a droppable is dropped. Fires the sort event.
		*/
		function handleAfterDrop (e) {
			var draggable = e.attachedTo,
				el = draggable.element;
			if (! el.prev().eq(this._previous || []) || ! el.parent().eq(this._parent))
				fire(this, "sort");
			delete this._prev;
			delete this._parent;
	    }

		/*
		Group: Functions
		*/

		Sortable.prototype = {
			/*
			Method: addItems (elements)

			Add items to the sortable. Should not contain items that are were a child of one
			of the containers when the sortable was created.

			Arguments:

				*elements* (glow.dom.NodeList/string/array of DOM Elements)
			  
				The elements to be added to the sortable.
			*/
			addItems : function (elements) {
				var this_ = this, opts = this._opts.draggableOptions;
				$(elements).each(function () {
					var draggable = new glow.dragdrop.Draggable(
						this, glow.lang.apply({
							placeholder       : 'none',
							axis              : this_.axis,
							container         : this_.constrainDragTo,
							dropTargets       : this_.dropTargets,
							acceptDropOutside : (this_.containers.length == 1)
						}, opts)
					);

					if (this_._opts.equaliseColumns)
					    addListener(draggable, 'drag', equaliseColumns, this_);

					addListener(draggable, 'drop', handleDrop, this_);
					addListener(draggable, 'afterDrop', handleAfterDrop, this_);

					this_.draggables.push(draggable);
				});
			}

		};

		return Sortable;
	}
});


;glow.module("glow.widgets.InfoPanel", "0.4.0", {
	require: [
		'glow.dom',
		'glow.events',
		'glow.widgets.Panel'
	],
	implementation: function() {
		var dom = glow.dom,
			$ = dom.get,
			events = glow.events,
			widgets = glow.widgets,
			lang = glow.lang,
			env = glow.env,
			win,
			positionRegex = /glow\-infoPanel\-point[TRBL]/,
			offsetInContextDefaults = {
				T: {x:"50%", y:"100%"},
				R: {x:0, y:"50%"},
				B: {x:"50%", y:0},
				L: {x:"100%", y:"50%"}
			};
			
		glow.ready(function() {
			win = $(window);
		});
		
		/*
		PrivateMethod: resolveRelative
			will work out x & y pixel values for percentage values within a context element.
			Non percentage values will just be passed back
		*/
		function resolveRelative(point, context) {
			var vals = [point.x, point.y],
				axis = ["x", "y"],
				sides = ["Width", "Height"],
				i = 0;
			
			//calculate for x & y
			for (; i < 2; i++) {
				if (vals[i].slice) {
					vals[i] = parseFloat(point[axis[i]]);
					if (point[axis[i]].slice(-1) == "%") {
						vals[i] = context[0]["offset" + sides[i]] * (vals[i]/100);
					}
				}
			}
			return {x: vals[0], y: vals[1]};
		}
		
		/*
		PrivateMethod: calculateBestPointerSide
			works out which side the pointer should appear on
		*/
		function calculateBestPointerSide(contextPos, contextSize) {
			//right, we need to work out where to put the box ourselves
			var scrollPos = widgets._scrollPos(),
				winSize = {x: win.width(), y:win.height()},
				//let's see how much free space there is around the context
				freeSpace = {
					T: winSize.y - contextPos.y - contextSize.y + scrollPos.y,
					R: contextPos.x - scrollPos.x,
					B: contextPos.y - scrollPos.y,
					L: winSize.x - contextPos.x - contextSize.x + scrollPos.x
				},
				preferenceOrder = ["T", "R", "B", "L"];
			
			preferenceOrder.sort(function(a, b) {
				return freeSpace[b] - freeSpace[a];
			});
			
			//could this be made clevererer? (which is why I did the preferenceOrder thing, the first may not always be best)
			return preferenceOrder[0];
		}

		/*
		Class: glow.widgets.InfoPanel
			A panel with content directed at a particular point on the page. This class extends
			<glow.widgets.Panel> (and therefore <glow.widgets.Overlay>).

		Constructor:
			(code)
			var myPanel = new glow.widgets.InfoPanel(content);
			(end)
			
		Arguments:
			content - (selector|DOMElement|NodeList) the element that contains the contents of the Panel. If this is
					   in the document it will be moved to document.body. If your content node has a child element with class "hd"
					   it will be added to the header of the panel. Similarly, an element with class "ft" will be added to the
					   footer of the panel.
			opts - (optional object) object containing the attributes specified below.

		opts:
		
			The options can contain zero or more of the following
			
			context - (selector|DOMElement|NodeList) element to position the panel's pointer at. If no context
				is provided then the panel must be positioned manually using the container property.
				
			pointerPosition - (string t|r|b|l) the side of the panel where the pointer should be. The default is dependant
				on the context's position on the page. The panel will try to be where it is most visible to the user.
				
			theme - (string) The theme to use, only applies when using the default template. Currently support themes are
				"dark" and "light". Default is "light"
				
			offsetInContext - (object) The position of the pointer within the context. This is in the format {x: 24, y: "50%"}.
				Pixel values or percentages can be used.
				The default points to an edge of the context, depending on the position of the pointer.
			
			pointerRegisters - (object) Only needed for custom templates. Identify the position of the point within pointer elements.
				See "Templating" below for more information.
			
			modal - (bool, default false) is the panel modal? If true then a default Mask will be created
					if one is not provided.
			
			autoPosition - (bool, default true) if true, the panel will be positioned automatically to the context element.
						   If false, you have to set the position of the panel manually using <setPosition>
			
			From <glow.widgets.Panel>:
			
			width - (number / string, pixel or percentage) width of the panel. Default 400px.
			
			template - (string) an html template to use to create the panel.
			
			From <glow.widgets.Overlay>:
					
			mask - (glow.widget.Mask) used to indicate to the user that the overlay is modal. If provided
					then the modal property is set to true.
					
			closeOnMaskClick - (bool, default true) if true then listens for a click event on the mask
								and hides when it fires.
								
			anim - (string / object / function, default null). Specifies a transition for showing / hiding the panel. Can be "fade" or "roll",
					or a function which returns a <glow.anim.Animation> or <glow.anim.Timeline>. The function is passed the overlay
					as the first parameter, and 'true' if the overlay is showing, 'false' if it's hiding.
								
			zIndex - (number, default 9991) the z-index to set on the overlay. If the overlay is modal, the zIndex
					 of the mask will be set to one less than the value of this attribute.
			
		Templating:
			
			Your template must contain elements with these class names to identify the header, body and footer of your layout
			
			glow-infoPanel-pointerT - Element used as the pointer on the top of the panel
			
			glow-infoPanel-pointerR - Element used as the pointer on the right of the panel
			
			glow-infoPanel-pointerB - Element used as the pointer on the bottom of the panel
			
			glow-infoPanel-pointerL - Element used as the pointer on the left of the panel
			
			From <glow.widgets.Panel>:
			
			glow-panel-hd - The header of the panel
			
			glow-panel-bd - The body of the panel
			
			glow-panel-ft - The footer of the panel
			
			glow-panel-close - The close button of the panel (a click event will be added to this element)
			
			If you provide a template, you may also want to provide 'pointerRegisters' to identify where the
			points within your pointer elements are. This is an object with properties 't', 'r', 'b' & 'l'.
			
			The default is
			
			(code)
			{
				t: {x: "50%", y: 0},
				r: {x: "100%", y: "50%"},
				b: {x: "50%", y: "100%"},
				l: {x: 0, y: "50%"}
			}
			(end)
			
		*/
		function InfoPanel(content, opts) {
			opts = opts || {};
			
			if (opts.template) {
				var customTemplate = true;
			}
			
			opts = glow.lang.apply({
				modal: false,
				theme: "light",
				autoPosition: !!opts.context,
				pointerRegisters: {
					t: {x: "50%", y: 0},
					r: {x: "100%", y: "50%"},
					b: {x: "50%", y: "100%"},
					l: {x: 0, y: "50%"}
				}
			}, opts);
			
			//deal with context if it's a selector
			opts.context = opts.context && $(opts.context);
			
			widgets.Panel.call(this, content, opts);
			
			if (!customTemplate) {
				this.content.addClass("glow-infoPanel");
			}

			this.content.addClass("glow-infoPanel-point" + (opts.pointerPosition || "t").toUpperCase());
		}
		lang.extend(InfoPanel, widgets.Panel);
		
		lang.apply(InfoPanel.prototype, {
			/*
			Method: setPosition
				Change or recalculate the position of the panel. Call with parameters to
				change the position of the panel or call without parameters to recalculate
				the position of the panel. You may need to call this without parameters
				if  you move the context element.
			
			Arguments:
				x - (number) pixel position from the left of the document to point at
				
				y - (number) pixel position from the top of the document to point at
			*/
			setPosition: function(x, y) {
				//don't use set position if autoPosition is false
				var valsPassed = (x !== undefined && !(x.source));
				
				if (!(this.autoPosition || valsPassed)) {
					return this;
				} else if (valsPassed) {
					this.autoPosition = false;
				}
				
				var opts = this.opts,
					contentNode = this.content[0],
					pointerPosition = opts.pointerPosition,
					context = opts.context,
					container = this.container,
					//here's what we need to position the pointer
					pointerElm,
					//this will hold the point the user passed, or the context's offset
					contextOffset = valsPassed ? {x:x, y:y} : context.offset(),
					contextSize = valsPassed ? {x:0, y:0} : {x:context[0].offsetWidth, y:context[0].offsetHeight},
					offsetInContext,
					pointOffsetInPanel,
					pointerInnerOffset,
					panelOffset = container.offset(),
					pointerOffset,
					lastPointerPosition;
					
				if (!pointerPosition) {
					//right, we need to work out where to put the box ourselves
					pointerPosition = calculateBestPointerSide(contextOffset, contextSize);
					if (lastPointerPosition != pointerPosition) {
						lastPointerPosition = pointerPosition;
						contentNode.className = contentNode.className.replace(positionRegex, "glow-infoPanel-point" + pointerPosition);
						pointerElm = container.get(".glow-infoPanel-pointer" + pointerPosition);
					}
				} else {
					pointerPosition = pointerPosition.toUpperCase();
				}
				
				if (!pointerElm) {
					pointerElm = container.get(".glow-infoPanel-pointer" + pointerPosition);
				}
				
				//get default offset if there isn't one
				offsetInContext = valsPassed ? {x:0, y:0} : resolveRelative(opts.offsetInContext || offsetInContextDefaults[pointerPosition], context);
				pointerInnerOffset = resolveRelative(opts.pointerRegisters[pointerPosition.toLowerCase()], pointerElm);
				pointerOffset = pointerElm.offset();
				pointOffsetInPanel = {x: pointerOffset.x - panelOffset.x + pointerInnerOffset.x, y: pointerOffset.y - panelOffset.y + pointerInnerOffset.y};
				
				container.css("left", contextOffset.x + offsetInContext.x - pointOffsetInPanel.x + "px").
						  css("top", contextOffset.y + offsetInContext.y - pointOffsetInPanel.y + "px");
						  
				if (env.ie < 7 && !opts.modal) {
					var overlayStyle = container[0].style;
					this._iframe.css("top", overlayStyle.top).
								 css("left", overlayStyle.left).
								 css("width", container[0].offsetWidth + "px").
								 css("height", container[0].offsetHeight + "px");
				}
				return this;
			},
			/*
			Method: setContext
				Sets the element the helper should point to.
				
			Arguments:
				context - (selector|DOMElement|NodeList) element to position the panel's pointer at.
			*/
			setContext: function(context) {
				this.opts.context = $(context);
				this.autoPosition = true;
				if (this.container[0].style.display == "block") {
					this.setPosition();
				}
				return this;
			}
		});
		
		return InfoPanel;
	}
});
/*@end @*/