﻿/// <reference path="jquery.intellisense.js"/>  
/**
 * Cobalt Elements for jQuery
 * flyout - this manages the expading/collapsing flyout.
 *
 * These methods are build on the jQuery and interface foundation to provider
 * client functionality to the Cobalt CMS system.
 *
 * Copyright (c) 2007 Scorpion Design, Inc.
 *
 */
(function($) {

	// Return a new instance of the cobalt editor.
	$.fn.Flyout = function(o) {

		this.each(function()
			{
				var el = $(this);
				if (el.is('a'))
				{
					// Migrate the page attributes to the parent and fly from there instead.
					var p = el.parent();
					p.attr('_pageid',el.attr('_pageid')||'')
						.attr('_childpages',el.attr('_childpages')||'')
						.attr('_exclude',el.attr('_exclude')||'')
						.attr('_corner',el.attr('_corner')||'')
						.attr('_maxitems',el.attr('_maxitems')||'')
						.attr('_maxlevels',el.attr('_maxlevels')||'');
					
					// Remove the attributes from the child.
					el.removeAttr('_pageid')
						.removeAttr('_childpages')
						.removeAttr('_exclude')
						.removeAttr('_corner')
						.removeAttr('_maxitems')
						.removeAttr('_maxlevels');

					// Reassign which element we're dealing with.
					el = p;
				}

				// If the flyout hasn't already been built, do so now.
				if (!el[0].flyout)
					new $.cobalt.flyout(el[0],o);
			});

		return this;
	};
	
	// Create the cobalt namespace if it has not already been done.
	$.cobalt = $.cobalt || {};

	// Tree contructor.
	$.cobalt.flyout = function(el,o) {

		// Ensure we have an actual element.
		if (!el) return;

		// Define the basic options.
		this.options = $.extend({},o||{});
		this.getChildrenUrl = this.options.getChildrenUrl||this.getChildrenUrl;

		// Initialize the flyout.
		this.element = el;
		this.init();
	};

	// Assign a global shortcut to this class.
	if (!window.$fo) { window.$fo = $.cobalt.flyout; }

	// Define static properties and methods.
	$.extend($.cobalt.flyout, {

		// HTML for building the trees.
		pageHtml : '<div class="ifly" _pageid="{0}" _childpages="{1}"><a href="{2}">{3}</a></div>',

		// flyout stack.
		flyouts : [],

		// Track the ajax calls
		ajaxip : false,
		ajaxStack : [],

		// Render superscripts in the page names.
		_superScript : new RegExp('\\^(\\S+)','g'),
		pageFormat : function(pageName)
			{
				return pageName.replace($fo._superScript,"<sup>$1</sup>");
			},

		_paused : false,

		// Pause the menu flyouts.
		pause : function()
			{
				$fo._paused = true;
				$fo.hideItems();
			},

		// Play the menu flyouts.
		play : function()
			{
				$fo._paused = false;
			},

		// When we mouse over an item.
		itemOver : function(e)
			{
				// Do nothing if the flyouts are paused.
				if ($fo._paused) return;

				// Get the item we just hovered over.  If we're over a child item, move up until we find the first item
				// With the p property defined.
				var item = e.target;
				while (item && !item._p) { item = item.parentNode; }

				// If we didn't find anything, cancel out.
				if (!item) return true;

				// Fly the current item.
				$fo.flyItem(item,e);
			},

		// Fly a specific item according to current conditions.
		flyItem : function(item,e)
			{
				var p = item._p;

				// If the parent has a timeout attached, clear it.
				if (p.timeout)
				{
					clearTimeout(p.timeout);
					p.timeout = 0;
				}
				
				if (p._active)
				{
					// If we've moused onto the current active item, do nothing.
					if (p._active==item && !$fo.fadeStarted)
						return;
					// If the other active item has a child menu showing, set a timeout just in case the user didn't intend to move off of it.
					else if (p._active._children)
					{
						// How close are we to the right edge of the menu.
						var pos = $(item).absPosition();
						var distance = (pos.left + pos.width) - e.clientX;

						if (distance>=0 && distance<50)
							// If we're really close to the edge, the user may be trying to move the mouse to the child menu.
							p.timeout = setTimeout(function(){$fo.showItem.apply(item,[e]);},250);
						else
							// Otherwise, fire it right away.
							$fo.showItem.apply(item,[e]);
						return;
					}
				}

				// Otherwise, show the just mouseover item right away.
				$fo.showItem.apply(item,[e]);
			},

		// We just moved off of the entire toolbar and all of its children.
		itemOut : function(e)
			{
				// Do nothing if the flyouts are paused.
				if ($fo._paused) return;

				// Get the item we just hovered over.  If we're over a child item, move up until we find the first item
				// With the p property defined.
				var p = e.relatedTarget;
				while (p && !p._p && !p._f) { try { p = p.parentNode; } catch(ex) { p = null; } }

				// If we're still over a flyout menu item, stop here.
				if (p && (p._p || p._f)) return true;

				// Clear any hide timeouts associated with the whole toolbar.
				if ($fo._timeout)
				{
					clearTimeout($fo._timeout);
					$fo._timeout = 0;
				}

				// Fade out the visible flyouts and hide them after a timeout.
				var levels = $fo.fadeOut();
				$fo._timeout = setTimeout($fo.hideItems,(300*levels)+800);
			},

		// Fade out the last visible child menu.
		fadeOut : function()
			{
				// Find the last flown child menu and fade it out.
				var active = $fo._active;
				var levels = 1;
				var fadeStarted = $fo.fadeStarted;
				while (active && active._children)
				{
					if (active._active && active._active._children)
					{
						active = active._active;
						levels++;
					}
					else
					{
						$fo.fadeStarted = true;
						var speed = 300 + (fadeStarted?0:800);
						active._children.animate({opacity:0},speed,'linear',function()
							{
								$fo.hideItem.apply(active,[{}]);
								$fo.fadeOut();
							});
						break;
					}
				}
				return levels;
			},

		// Stop any fade animations.
		stopFadeOut : function()
			{
				// Clear any hide timeouts associated with all flyouts.
				if ($fo._timeout)
				{
					clearTimeout($fo._timeout);
					$fo._timeout = 0;
				}

				if ($fo.fadeStarted)
				{
					var active = $fo._active;
					while (active && active._children)
					{
						active._children.stop().css({opacity:1});
						active = active._active;
					}
					$fo.fadeStarted = false;
				}
			},

		// Show a specific item.
		showItem : function(e)
			{
				// Just in case.
				$fo.stopFadeOut();

				// Clear the parent timeout counter (as this method just fired).
				this._p.timeout = 0;

				// See if we need to scroll the list up or down.
				$fo.setScroll(this,e);

				// If a different branch at this same level is active, hide it.
				var active = this._p._active;
				if (active && active!=this) $fo.hideItem.apply(active,[{}]);
				this._p._active = this;

				// Set the item as active
				this.isactive = true;
				this._self.addClass('iflyOver');

				// Show any child elements.
				if (this._callajax)
				{
					if (this._killajax)
						// Since we've moved BACK onto the element, we no longer need to kill it.
						this._killajax = false;
					else if (!this._ajaxip)
						// Otherwise, if we haven't started the ajax call, do so now.
						this.flyout.loadChildren.call(this.flyout,this,this.pageid);
				}
				else
				{
					var div = this._children;
					if (div && div.length)
					{
						// Position the child.
						var corner = this._corner||$._DIR._UPPER_RIGHT;
						var dir = this._dir||$._DIR._LOWER_RIGHT;
						var isreverse = {};
						div.css({opacity:0})
							.relativePos(this,corner,dir,this._offset||{left:-1,top:-1},null,isreverse)
							.animate({opacity:1},100,'linear');

						// If we have more items than the max, set them up for an auto-scroll.
						var max = this.flyout.options.maxitems;
						var size = div[0].childNodes.length;
						if (max && size>max)
						{
							// If we haven't yet recorded the scroll height.
							if (!div[0]._scrollHeight)
							{
								var h = div.children(':first').absPosition().height;
								div.css({height:max*h}).addClass('iflyOverflow');
								div[0]._scrollHeight = (size-max)*h;
							}

							// Record its position and scroll status.
							div[0]._pos = div.absPosition();
							div[0]._pos.top += (50+$.toInt($(document.body).css('paddingTop')));
							div[0]._pos.height -= 100;
							div[0]._scrollTop = 0;

							// Reset any previously moved child elements.
							div[0].childNodes[0].style.marginTop = '0px';
						}

						// If we are flying this element in reverse.
						if (isreverse.left)
							this._self.addClass('iflyLeft');
						else
							this._self.removeClass('iflyLeft');
					}
				}

				// Show any parent elements this belongs to.
				var item = this._p;
				while (item && item._p) {
					$fo.flyItem(item,e);
					item = item._p;
				}
			},

		// If a div is taller than its visible area, scroll it up and down in time with the mouse movements.
		setScroll : function(item,e)
			{
				var div = item.parentNode;
				if (e.clientY && div._scrollHeight)
				{
					var top = parseInt((e.clientY-div._pos.top)/div._pos.height*div._scrollHeight);
					if (top<0)
						top = 0;
					else if (top > div._scrollHeight+2)
						top = div._scrollHeight+2;

					div._scrollTop = top;
					div.childNodes[0].style.marginTop = '-'+top+'px';
				}
			},

		// Hide a specific item.
		hideItem : function(e)
			{
				// Clear any timeouts associated with the element.			
				if (this._p)
				{
					if (this._p.timeout)
					{
						clearTimeout(this._p.timeout);
						this._p.timeout = 0;
					}
					this._p._active = null;
				}

				// If it is active, remove the class and hide any children.
				if (this.isactive)
				{
					this.isactive = false;
					this._self.removeClass('iflyOver');
					if (this._children)
					{
						// Recurse this method on any child elements.
						this._children.children().each($fo.hideItem);
						
						// Then hide the menu.
						this._children.hide();
					}
				}

				// If we are in the middle of an ajax call, kill it.				
				if (this._ajaxip)
					this._killajax = true;
			},

		// Hide every flyout that is active.
		hideItems : function(e)
			{
				$($fo.flyouts)
					.each($fo.hideItem)
					.find('div.ifly')
						.each($fo.hideItem);
			},

		// Navigate to the href
		navigatePage : function(e)
			{
				// If the event target is an anchor tag, than let the default event take it's course.
				if (e&&e.target&&e.target.nodeName&&e.target.nodeName.toLowerCase()=='a')
					return;
				else
				{
					// Otherwise navigate to the href of the anchor.
					var href = $(this).children('a').attr('href');
					if (href) window.location.href = href;
					return false;
				}
			}
	});

	// Extend the flyout prototype with these shared properties/methods.
	$.extend($.cobalt.flyout.prototype, {

		// Ajax
		getChildrenUrl : 'CobaltAjax.ashx?M=GetPage',

		// Load the children of the element.
		loadChildren : function(el,pageid)
			{
				var fo = this;
				var url = $.getAjaxUrl(fo.getChildrenUrl,{P:pageid});
				if (url)
				{
					el._self.addClass('iflyLoading');
					el._ajaxip = true;
					
					// Build a private function to handle the ajax call.
					var fn = function()
					{
						$fo.ajaxip = true;
						$.ajax(
							{
								url:url,
								dataType:'json',
								success:function(data){fo.renderChildren(data,el);},
								error:function(){fo.renderChildren(null,el);}
							});
					};
					
					// Rather than tying up the UI with a bunch of ajax calls, we'll stack them up and call them one at a time.
					if ($fo.ajaxip)
						$fo.ajaxStack.push(fn);
					else
						// Otherwise if the stack is empty, just call it straight away.
						fn();

					return;
				}
			},

		// Render the children.
		renderChildren : function(results,el)
			{
				// If we have ajax calls in the stack, pull the first one out and call it, then finish this method.
				if ($fo.ajaxStack.length)
					$fo.ajaxStack.shift()();
				else
					$fo.ajaxip = false;

				// If we have no results, exit.
				el._callajax = false;
				if (!results) return;

				// We no longer need the loading class.
				el._self.removeClass('iflyLoading');

				// Ensure we have the children defined.
				if (!el._children || !el._children.length)
					el._children = $('<div class="iflylist"></div>').appendTo(document.body);

				// Get any exclusions.
				var ex = el.flyout && el.flyout.options && el.flyout.options.exclude;

				// Iterate through the child pages and add each one.
				for (var i=0;i<results.ChildPages.length;i++)
				{
					// Get the next page
					var page = results.ChildPages[i];

					// If this page is excluded, skip over it.
					if (ex && ex.indexOf(page.PageID)>=0)
						continue;

					var item = $($fo.pageHtml
						.replace('{0}',page.PageID)
						.replace('{1}',page.ChildPages.length)
						.replace('{2}',page.URL||page.Path)
						.replace('{3}',$fo.pageFormat(page.PageName))).appendTo(el._children);

					// Assign any of the URL Properties.
					if (page.URLProperties.indexOf('target,_blank')>=0)
						item.children('a').attr('target','_blank');
				}

				// Setup the new branch.
				this.setupChildren(el);

				// Note that we have finished the ajax call for this one element.
				el._ajaxip = false;

				// If we have moused off the element and killed the ajax, do not fire the hover event.
				if (el._killajax)
					el._killajax = false;
				else
					$fo.showItem.apply(el,[{target:el}]);
			},

		// Set up the properties and events of the flyout.
		setupChildren : function(el)
			{
				// Assign the basic properties to the element.
				if (!el._self) el._self = $(el);
				el.flyout = this;
				el.pageid = $.toInt(el._self.attr('_pageid'));

				// Set up any child elements.
				if (el._children)
					el._children
						.bind('mouseover',$fo.itemOver)
						.bind('mouseout',$fo.itemOut)
						.each(function(i){this._f=el;});

				// If this is a page-based flyout.
				if (el.pageid)
				{
					// Get any child lis and recurse this method.
					var fo = this;
					var lis = el._children && el._children.find('>div.ifly')
						.click($fo.navigatePage)
						.each(function(i)
							{
								// Get a ref to the item's parent container for mouseover scripts.
								this._p = el;
								this._levels = el._levels-1;

								// Recurse this method to set the child elements.
								fo.setupChildren(this);
							});

					if (el._levels!=0)
					{
						if (lis && lis.length)
							// If we have child elements, add the fly right class.
							el._self.addClass('iflyRight');
						else
						{
							// If we SHOULD have children, note that we'll pull them with an ajax call
							if ($.toInt(el._self.attr('_childpages'))>0)
							{
								el._callajax = true;
								el._self.addClass('iflyRight');
							}
						}
					}

					// Add an MSIE6 fix.
					if ($.browser.msie6)
						el._self.addClass('iflyie6');
				}
				else if (!el._children || !el._children.length)
					// If it isn't a page-based flyout, then the first div child will be the flyout.
					el._children = el._self.find('>div:first');
			},

		// Initialize the flyout.
		init : function()
			{
				// Add this element to the global flyout stack
				var el = this.element;
				$fo.flyouts.push(el);

				// The parent element will be static flyout class, so that all flyouts are treatest as a collective group.
				el._p = $fo;

				// Setup the element and its properties.
				el._self = $(el);
				el._children = el._self.find('div.iflylist:first');

				// How many levels down should we go?
				el._levels = $.toInt(el._self.attr('_maxlevels'))||this.options.maxlevels||-1;

				// Set up the child elements.
				this.setupChildren(el);

				// Define the starting position of the first flyout.
				var corner,dir;
				try { corner = eval('('+el._self.attr('_corner')+')'); } catch (ex) {;}
				try { dir = eval('('+el._self.attr('_dir')+')'); } catch (ex) {;}
				el._corner = this.options.corner||corner||$._DIR._LOWER_LEFT;
				el._dir = this.options.dir||dir||$._DIR._LOWER_RIGHT;

				// How many items to display before we start auto-scrolling the contents when the mouse moves.
				if ($.browser.msie6)
					this.options.maxitems = 0;
				else
					this.options.maxitems = $.toInt(el._self.attr('_maxitems'))||16;

				// Record any exclusions.
				if (!this.options.exclude)
				{
					this.options.exclude = [];
					var exclude = (el._self.attr('_exclude')||'').split(',');
					for (var i=0;i<exclude.length;i++)
					{
						var id = $.toInt(exclude[i]);
						if (id) this.options.exclude.push(id);
					}
				}

				// If this is a page flyout, and we don't have the first level already, get it with an ajax call.
				if (el.pageid && !el._children.length)
				{
					el._killajax = true;
					this.loadChildren(el,el.pageid);
				}

				// Set up the hover actions.
				el._self
					.bind('mouseover',$fo.itemOver)
					.bind('mouseout',$fo.itemOut);
			}
	});
	
})(jQuery);
