
// fix JS errors due to function on SELECT#all_countries
// function is writteon on those pages that need it (see form_functions.php)
if (!window.change_state_box)
	window.change_state_box = function(){ return true; };

/* Ajax defaults
 * CANNOT use 10-second timeout because some AdServer $.get's are very slow!
 *
$.ajaxSetup({
	timeout:	10000	// default timeout for $.get/$.load
});
*/

$(document).ready(function(){

	//return; // DISABLE ALL JAVASCRIPT FUNCTIONALITY

	// init form-field validator
	Validation.init();

	/* init special navigation
	 * - floating Crumbbar 
	 * - WordPress Nav3 flyout-menus
	 */
	Nav.init();

	// this will be called by pages that need it - with options
	//$('.collapsible-by-heading').each( Collapsible.initByHeading );

	// Add-This initialization (currently inits onLoad)
	//addthis.init(); used only if script loaded with: #async=1

	/* init content-specific fixes
	 * - IE9 needs a 'wrapper' around elems that have BOTH a filter/gradient and radiused-corners
	 */
	Content.init();

	// init Homepage slideshow
	initSlideshow();

	/* init drop-down navigation menus
	 * - mega-menus on primary navbar
	 * - simple drop-downs on breadcrumb items
	 */
	//Navbar.init();

	// modify addThis buttons
	Content.initSocialMedia();
});


function sayHello () { alert('Hello'); }

function initSlideshow () {
	// SEE http://jquery.malsup.com/cycle/options.html
	var	$c 	= $('#article-top-slideshow > div')
	,	$i	= $c.children('img:first');
	if (!$c.length) return;
	// give container a width & height before starting slideshow or it will 'collapse'
	$c	.css({ width: $i.width(), height: $i.height() })
		.cycle({
			fx:			'turnRight'	// scrollLeft, turnDown, curtainX, fade, zoom, shuffle, 
		//,	easing:		'easeOutBounce'
		,	delay:		2000	// _extra_ transition delay on first slide
		,	timeout:	5000	// ms between transitions
		,	pause:		true	// true = pause the cycler when over a slide
		});
};


/*
 *	Content - UI Manipulation
 *
 *	
 */
var Content = {

	localScrollOptions: {
		// filter out URLs with ajax-fragment links - not a real bookmarks!
		filter:		function(){ $A=$(this); return (this.href.match(/#!/) || $A.parent().parent().hasClass('ui-tabs-nav')) ? false : $A.attr('title')!=='Go to Top'; }
	,	duration:	500		// total scroll time
	,	hash:		false	// false = DO NOT show bookmark as URL hash
	,	offset: 	(($('#crumbbar_wrapper').outerHeight() || 0) * -1) - 9 // keep elem 10px below top (eg: fixed crumbbar)
	//,	easing:		'elasout'	
	}

,	state: {
		localScrollInitialized:		false
	}

	// trigger all methods that should be called after the page-font-size has been changed
,	updateFontDependentElements: function(){
		Content.equalizeBoxHeights();
		Content.equalizeToolThumbnailHeights();
		if (window.resizeScroller) resizeScroller();
	}

,	init: function () {
		var _ = Content;
		_.pushFooterToBottom();
		_.equalizeBoxHeights();
		_.equalizeToolThumbnailHeights();
		_.wrapRadiusedElems();

		// add delay before scrolling to bookmark so page has time to scroll *normally* before we 'adjust it'
		setTimeout(function(){
			Content.scrollToBookmarks.hash( document.location.hash );
			Content.scrollToBookmarks.init(); // if not already initialized with custom options
		}, 1000);

		$(window).resize(function(){ Timer.set('', Content.pushFooterToBottom, 750); });
	}

,	pushFooterToBottom: function () {
		var page	= $(window).height()
		,	head	= $('div.page_header').outerHeight() || 0
		,	foot	= $('div.page_footer').outerHeight() || 0
		,	$C		= $('div.page_content')
		,	vPad		= parseInt( $C.css('paddingBottom'), 10 )
		;
		$C.css( 'minHeight', page - head - foot - vPad );
	}

//	add a DIV wrapper around radiused elements so IE9 gradient backgrounds do not overflow corners
,	wrapRadiusedElems: function (elems) {
		// this is needed for IE9 ONLY
		if (!$.browser.msie || $.browser.version < 9 || $.browser.version >= 10) return;
		if (!elems) elems = ''
			+  '.radius-wrap-me'
			+', .btn-submit'
			+', .box-panel > h5'
			+', h1.section-divider b'
			;
		$(elems).wrap(function(){
			var $E		= $(this)
			,	style	= ''
			,	vert	= ['top','bottom']
			,	horz	= ['left','right']
			,	attr	= ['width','style','color']
			,	i, k, s, v
			;
			for (i=0; i<=1; i++) {
				// border-radius
				for (k=0; k<=1; k++) {
					s = 'border-'+ vert[i] +'-'+ horz[k] +'-radius';
					v = parseInt( $E.css(s), 10 );
					if (v) style += s +":"+ v +'px; ';
				}
				// margin
				s = 'margin-'+ vert[i];
				v = $E.css(s);
				if (v) style += s +":"+ v +'; ';
			}
			// border width/style/color
			for (i=0; i<=1; i++) {
				for (k=0; k<=2; k++) {
					s = 'border-'+ vert[i] +'-'+ attr[k]; // eg: border-top-width
					v = $E.css(s);
//if (typeof elems === 'object') alert( s +' = '+ v );
					if (!v || v === '0px' || v === 'none') break;
					style += s +":"+ v +'; ';
				}
				for (k=0; k<=2; k++) {
					s = 'border-'+ horz[i] +'-'+ attr[k]; // eg: border-left-width
					v = $E.css(s);
					if (!v || v === '0px' || v === 'none') break;
					style += s +":"+ v +'; ';
				}
			}
			for (i=0; i<=1; i++) {
				// margin
				s = 'margin-'+ horz[i];
				v = $E.css(s);
				if (v) style += s +":"+ v +'; ';
			}
			// match 'float' & 'display' settings of elem being wrapped
			style	+=	'float:'+ $E.css('float') +'; ' // in case floated left/right
					+	'display:'+ $E.css('display') +'; '; // in case is inline-block
//if (typeof elems === 'object') alert( 'style = '+ style );
			return '<div class="radius-wrapper" style="'+ style +'"></div>';

			alert(
				'border-radius = '+				$E.css('border-radius')
			+'\n border-top-left-radius = '+	$E.css('border-top-left-radius')
			+'\n border-top-right-radius = '+	$E.css('border-top-right-radius')
			); 
		});
	}

,	scrollToBookmarks: {
		autoInit: function ( opts ) {
			$(document).ready(function(){ Content.scrollToBookmarks.init( opts ); });
		}
	,	init: function ( opts ) {
			var s = Content.state;
			if (s.localScrollInitialized) return; // have already initialized scrolling
			//debugData( $.extend( Content.localScrollOptions, opts || {} ), 'options' );
			$.localScroll( $.extend( Content.localScrollOptions, opts || {} ) );
			s.localScrollInitialized = true;
		}
	,	hash: function ( hash, opts ) {
			// SKIP if hash is missing OR an url_fragment
			if ( !hash || !hash.match(/^#[a-z]/) ) return;
			var el = $(hash)[0] || $('a[name="'+ hash.substr(1) +'"]')[0];
			if (el !== undefined) $(window).scrollTo( el, $.extend( {}, Content.localScrollOptions, opts ) );
		}
	}

,	changeFontSize: function (val) {
		var $body	= $('body')
		,	size	= parseInt( $body.css('fontSize'), 10 ) + parseInt( val, 10 )
		,	min		= 10
		,	max		= 16
		;
		if (size >= min && size <= max) {
			$body.css('fontSize', size +'px' );
			Cookie.set('ui_fontsize', size, 'never');
			setTimeout( Content.updateFontDependentElements, 500 );
		}
		else // beyond the size limits, so FIX the size for use below...
			size = Math.min(max, Math.max(min, size) );

		// see if this was called from a font-sizing button, and configure if so
		if (this.tagName === 'BUTTON') {
			this.blur();
			// GET button elements
			var UP,DN,txt, disabled='disabled disabled-opacity';
			if (val > 0) { // INCREASE SIZE was clicked
				UP = $(this)[0];
				DN = $(this).parent().prev().children()[0];
			}
			else { // DECREASE SIZE was clicked
				DN = $(this)[0];
				UP = $(this).parent().next().children()[0];
			}
			// SET INCREASE Button
			if (size === max) {
				UP.title = UP.value.after('|') +' '+ max +'px';
				UP.disabled = true;
				$(UP).addClass(disabled);
			}
			else {
				UP.title = UP.value.before('|') +' '+ (size+1) +'px';
				UP.disabled = false;
				$(UP).removeClass(disabled);
			}
			// SET DECREASE Button
			if (size === min) {
				DN.title = DN.value.after('|') +' '+ min +'px';
				DN.disabled = true;
				$(DN).addClass(disabled);
			}
			else {
				DN.title = DN.value.before('|') +' '+ (size-1) +'px';
				DN.disabled = false;
				$(DN).removeClass(disabled);
			}
		}
	}

//	equalizeBoxHeights gives 2 or more divs the same min-height so they align nicely (like on Homepages)
//	ONE of the divs has: class="equalize-height" siblings="[selector for OTHER divs in this group]"
//	OR may have class: "minimum-height", meaning that _only_ this element should have minHeight set
,	equalizeBoxHeights: function (source_elem, sibling_selector) {
		var selector, $this, $other, minHeight, maxHeight;
		$(source_elem || '.equalize-height,.minimum-height').each(function(){
			$this = $(this).css('minHeight',0); // RESET
			// NOTE: sibling_selector can be a string OR an element OR a jQuery object
			selector = sibling_selector || $this.attr('siblings');
			if (selector) {
				maxHeight = $this.outerHeight();
				$other = $(selector).not(this).each(function(){
					maxHeight = Math.max( maxHeight, $(this).css('minHeight',0).outerHeight() );
				});
				if (maxHeight) {
					$this.css('minHeight', Content.cssHeight( this, maxHeight ) ); // set min-height for THIS element
					if (!$this.hasClass('minimum-height'))	// SKIP this for 'minimum-height' class
						$other.each(function(){				// also set minHeight on the OTHER element(s)
							$(this).css('minHeight', Content.cssHeight( this, maxHeight ) );
						});
				}
			}
		});
	}

,	equalizeToolThumbnailHeights: function () {
		// each "row" of tool thumbs has a div-wrapper - equalize the children of each wrapper
		$(".tool-promo-listing > div").each(function(){
			var $c = $(this).children();
			Content.equalizeBoxHeights( $c.filter(":first"), $c );
		})
	}


,	initSocialMedia: function () {
		if (!window.addthis) return; // sharebar not loaded on this page
		$('#crumbbar,#sharebar').css({ overflow: 'visible' });
		var buttons = {
			addthis:	{ selector: '#share_addthis',	vOffset: -1 }
		,	twitter:	{ selector: '#share_twitter',	vOffset: -1 }
		,	facebook:	{ selector: '#share_facebook',	vOffset: 2 }
		,	google:		{ selector: '#share_google',	vOffset: -1 }
		};
		for (var x in buttons) Content.initShareButton( buttons[x] );

		// readjust position of drop-down ('compact') media list
		// normal code is inconsistent between browsers AND cannot handle the fixed-crumbbar at all!
		addthis.addEventListener('addthis.menu.open', function (evt) {
			var $popup = $('#at15s');
			if (evt.data.pane === 'compact') {
				if (Nav.state.isCrumbbarFloating)
					$popup.css({ position: 'fixed' }); // FIX positionn to match fixed crumbbar
				$popup.position({ // reposition popup
					my:			'left top'
				,	at:			'left bottom'
				,	of:			buttons.addthis.selector
				,	offset:		'0 3'
				,	collision:	'none'
				});
				$popup.css({ background: '#D5472B'})
			}
		});
		addthis.addEventListener('addthis.menu.close', function (evt) {
			if (evt.data.pane === 'compact') {
				var $popup = $('#at15s');
				$popup.css({ position: 'absolute' }); // RESET
			}
		});
	}

,	initShareButton: function (btn) {
		var $Button = $( btn.selector );
		if (!$Button.length) return;

		var $Iframe = $Button.children(':first'); // actually a DIV for Google, but is irrelevant
		if (!$Iframe.length || $Iframe.height() < 40) {
			setTimeout(function(){ Content.initShareButton(btn); }, 300 );
			return;
		}

		var $Wrap			= $('#crumbbar_wrapper')
		,	wrapZindex		= $Wrap.css('zIndex')
		,	buttonHeight	= 22
		,	iframeHeight	= $Iframe.outerHeight()
		,	marginTop		= buttonHeight - iframeHeight + (btn.vOffset || 0)
		,	b_normal		= { height: buttonHeight, marginTop: '-3px' } // , background: '#FF0'
		,	b_hover			= { height: iframeHeight, marginTop: (marginTop - 3) +'px' }
		,	i_normal		= { marginTop: marginTop +'px' }
		,	i_hover			= { marginTop: 0 }
		;
		$Iframe.css( i_normal );
		$Button.css( b_normal )
		.css({
			overflow:	'hidden'
		,	display:	'inline-block'	// also float:left
		,	position:	'relative'
		})
		.hover(
			function(){ $Button.css( b_hover );  $Iframe.css( i_hover );  if (!($Wrap.css('zIndex') > 2)) $Wrap.css({ zIndex: 11 }); }
		,	function(){ $Button.css( b_normal ); $Iframe.css( i_normal ); if ($Wrap.css('zIndex') === 11) $Wrap.css({ zIndex: 'auto' }); }
		);
	}


,	cssWidth: function (el, outerWidth) {
		if (outerWidth <= 0) return 0;
		if (!$.support.boxModel) return outerWidth;
		var $E = $(el)
		,	b = Content.borderWidth
		,	n = Content.cssNum
		// strip border and padding from outerWidth to get CSS Width
		,	W = outerWidth
			- b($E, 'Left')
			- b($E, 'Right')
			- n($E, 'paddingLeft')		
			- n($E, 'paddingRight')
		;
		return Math.max(0,W);
	}

,	cssHeight: function (el, outerHeight) {
		// a 'calculated' outerHeight can be passed so borders and/or padding are removed if needed
		if (outerHeight <= 0) return 0;
		if (!$.support.boxModel) return outerHeight;
		var $E = $(el)
		,	b = Content.borderWidth
		,	n = Content.cssNum
		// strip border and padding from outerHeight to get CSS Height
		,	H = outerHeight
			- b($E, 'Top')
			- b($E, 'Bottom')
			- n($E, 'paddingTop')
			- n($E, 'paddingBottom')
		;
		return Math.max(0,H);
	}

,	cssNum: function (el, prop, allowAuto) {
		if (el.jquery) el = el[0];
		var p, n;
		$.swap( el, { visibility: "hidden", display: "block", margin: '-9999px 0 0 -9999px' }, function(){
			p = $.css( el, prop, true );
			n = allowAuto && p=='auto' ? p : (parseInt(p, 10) || 0);
		});
		return n;
	}

,	borderWidth: function (el, side) {
		if (el.jquery) el = el[0];
		var b = 'border'+ side.substr(0,1).toUpperCase() + side.substr(1); // left => Left
		return $.curCSS(el, b+'Style', true) == 'none' ? 0 : (parseInt($.curCSS(el, b+'Width', true), 10) || 0);
	}

//	Also see $.swap - undocumented CSS utility
,	showInvisibly: function (el, force) {
		var $E = $(el)
		,	CSS = {
				display:	$E.css('display')
			,	visibility:	$E.css('visibility')
			};
		if (force || CSS.display == 'none') { // only if not *already hidden*
			$E.css({ display: 'block', visibility: 'hidden' }); // show element 'invisibly' so can be measured
			return CSS;
		}
		else return {};
	}

	// SUBROUTINE - works only for standard form-table layouts
,	getFieldLabel: function (field, defLabel) {
		var $I = is('String', field) ? $Form.find(':input').filter('[name="'+ field +'"]') : $(field);
		if (!$I.length) return defLabel || field;
		var defLabel = defLabel || $I.attr('name') || $I.attr('id')
		,	$L = $I.siblings('LABEL:first'); // <LABEL>Display</LABEL> <INPUT />
		if (!$L.length) $L = $I.closest('TD').prev().children('LABEL'); // <TH><LABEL>Display</LABEL></TH> <TD><INPUT /></TD>
		return $L.length ? $L.text() : defLabel;
	}

,	appendTooltips: function (selector, target) {
		$( selector || 'div.atip' )
			.appendTo( target  || '#mystickytooltip > div')
			.addClass('atip'); // in case not already has
	}

};


/*
 *	Validation - Generic Form Validation
 *
 *	uses hidden fields in form to store display-name, data-type and is-required flag
 */
var Validation = {
	options: {
		msgRequiredFields:	'Your information could not be submitted:'
	}	

,	forms:			{}
,	errorFields:	[] // will be set from PHP in calling page, if there are any

,	fieldDefaults: {
		name:		''	// fieldname
	,	dis:		''	// display name
	,	type:		''	// field type, eg: 'input:text', 'textarea'
	,	loadValue:	''	// value on-page-load
	,	checked:	0	// Is Checked ? (radio or checkbox only)
	,	req:		0	// Required Field ?
	,	re:			''	// Regular Expression for validation
	,	val:		'str'	// data-type
	,	min:		1		// min length or val
	,	max:		99999	// max length or val
	}

,	validate:	{ // v = field-value, d = field-data (JSON)
		str:	function (v,d){ l=v.length; return l >= d.min && l <= d.max; }
	,	txt:	function (v,d){ return Validation.validate.str(v,d); }
	,	eml:	function (v,d){ return $.trim(v).match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i); }
	,	phn:	function (v,d){ return $.trim(v).match(/.*[0-9]{2}.*[0-9]{3}.*[0-9]{4}.*/); } // TODO: Test Me!
	,	date:	function (v,d){ return Validation.validate.str(v,d); } // TODO: add RegExp for dates
	,	'int':	function (v,d){ return v==parseInt(v,10) && v >= d.min && v <= d.max; }
	,	dec:	function (v,d){ return v==parseFloat(v) && v >= d.min && v <= d.max; }
	,	bool:	function (v,d){ return v==!!v; }
	}

,	init: function (forms, fieldNames) {
		$( typeof forms === 'string' ? forms : 'form.validate-me' ).each(function(){
			if (!this.tagName === 'FORM' || !this.name) return false;
			Validation.forms[ this.name ] = { name: this.name, fields: {} }; // init data hash
			Validation.initFields( this, fieldNames );
			$(this).filter('.no-submit-on-enter').keypress( preventFormSubmitOnEnter );
		});
	}

,	initFields: function (FORM, fieldNames) {
		var _ = Validation;
		if (fieldNames) fieldNames = fieldNames.replace(/,/,' ');
		$(FORM.elements).not('[type=hidden]').each(function(){
			var name	= this.name
			,	e_data	= FORM.elements['data_'+ name]
			,	field	= _.forms[ FORM.name ].fields[ name ]
			,	type	= this.tagName.toLowerCase() + (this.tagName === 'INPUT' ? ':'+ this.type : '') // eg: 'input:text', 'input:radio'
			,	JSON
			;
			if (fieldNames) {
				if (!fieldNames.match(( new RegExp('\b'+ name +'\b') )))
					return true; // CONTINUE / SKIP
			}
			if (!field || this.checked) { // if already exists (duplicate fieldname) then may be a set of radio buttons
				// add *or replace* field data
				field = _.forms[ FORM.name ].fields[ name ] = $.extend( {}, _.fieldDefaults, {
					name:		name
				,	dis:		name
				,	type:		type
				,	loadValue:	this.value
				,	checked:	(type==='input:radio' || type==='input:checkbox' ? !!this.checked : '')
				});
				// look for a hidden field with exta data, eg: <input value="DisplayName|s|1"
				if (e_data) {
					try {
						JSON = $.parseJSON(e_data.value);
						//debugData( JSON, 'JSON' );
						$.extend( field, JSON );
						if (!JSON.dis) field.dis = Content.getFieldLabel( this, field.dis );
					} catch (ex) {}
				}
			}
			// add pointer to field-data branch
			$(this)	.data( 'fieldData', Validation.forms[ FORM.name ].fields[ name ] )
					.change( Validation.onChange );
		});
	}

	// called onLoad if a form submission had errors (call output by PHP)
,	highlightErrorFields: function (FORM, errorFields) {
		var errs = errorFields || Validation.errorFields
		,	i, c = errs ? errs.length || 0 : 0
		;
		for (i=0; i<c; i++)
			$(':input').filter('[name="'+ errs[i] +'"]').not('[type=hidden]').addClass('invalid');
	}

,	validateFields: function (FORM, fieldNames, showMsg) {
		if (!FORM || !FORM.elements) return false; // ERROR

		if (fieldNames === false)
			showMsg = false;
		else if (fieldNames === '')
			return true; // if a blank list of fields is passed, then nothing to validate!
		else if (is('String', fieldNames))
			fieldNames = fieldNames.replace(/\,/g,' ');

		var _ = Validation
		,	fail = false, counter = 1, msg = '', $E, value, data
		;
		_.errorFields = []; // CLEAR		

		$(FORM.elements).not('[type=hidden]').each(function(){
			if (fieldNames) {
				if (!(new RegExp('\\b'+ this.name +'\\b')).test( fieldNames ))
					return true; // CONTINUE / SKIP
			}
			$E = $(this);
			data = $E.data('fieldData');
			//debugData( data, 'data' );
			if (data && data.req) {
				value = $E.val();
				// Note: if a checkbox is specified "required", then it *MUST be checked*, otherwise required is meaningless!
				if (!value.length || (data.type==='input:checkbox' && !$E.is(':checked'))) // (( - to make code collapse!
					msg += '\n\n '+ counter++ +')  '+ data.dis +' is required';
				else if (!Validation.validate[ data.val ]($E.val(), data) && data.type!=='input:radio') // TODO: handle radio buttons better
					msg += '\n\n '+ counter++ +')  '+ data.dis +' entered is not valid';
				else
					return true; // NEXT FIELD
				$E.addClass('invalid');
				if (!fail) $E.focus(); // focus first error field
				fail = true;
				_.errorFields.push( this.name );
			}
		});
		//alert( 'validateFields.fail = '+ fail );
		if (fail && showMsg !== false)
			alert( _.options.msgRequiredFields + msg +'\n ' );
		return !fail;
	}

,	onChange: function () {
		var	$E		= $(this)
		,	value	= this.value
		,	data	= $E.data('fieldData')
		,	valid	= !data || Validation.validate[ data.val ](value, data)
		;
		if (valid)	$E.removeClass('invalid');
		else		$E.addClass('invalid');
	}

,	onSubmit: function () {
		return Validation.validateFields( this );
	}

};


/*
 *	Tool namespace
 *	Generic methods used by tool-pages to display messages, results, etc.
 */
var Tool = {
//	customize language used in messages
	default_language:	'EN'
,	language:			false
,	text: {
		EN: {
			tool_name:		''
		}
	}

//	jQuery selectors for key elements
,	el_result_header:	'h3.result-header'
,	el_result_wrapper:	'div.result-wrap'
,	el_result_loading:	'.loading'
,	el_result_output:	'.result'
,	el_result_error:	'.infoo_custom'

,	lastHashValue:		''
,	ajaxSubmitXHR:		null	// Ajax request object
,	initialized:		false

,	options:			{}
,	default_options: {
		language:		''		// blank = use Tool.default_language -- probably ALWAYs blank!
	,	focusField:		''		// blank = focus 'first visible field', else pass a selector-string
	,	formSelector:	'form.tool-options' // jQuery selector for FORM element
	,	required:		''		// list or array of required fields - used by submitForm()
	,	pollHash:		false	// true = check the location.hash every second to check for changes - handles the BACK button???
	,	setHash:		false	// true = when tool is submitted, create & set location.hash from the field data
	,	hashFields:		''		// comma-delimited list of fields to include in hash - blank = ALL 'visible' fields
	,	pollCallback:	''		// callback function for handling hash-changes (SEE Tool.pollHash)
	,	collapseContent: true	// this value passed to Collapsible - initCollapsed option
	}


,	autoInit: function ( opts ) {
		$(document).ready(function(){
			var _ = Tool;
			// init Tool instance/object, including options
			_.init( opts );
			// focus first form-field
			$((opts.formSelector || _.options.formSelector) +' :input:visible').each( resetDefaultValue ).eq(0).focus();
			// init collapsible content - used on most tool pages
			$('.tool-info .collapsible-by-heading').each(function(){
				Collapsible.initByHeading.call(this, { animate: true, initCollapsed: _.options.collapseContent });
			});
		});
	}

,	init: function ( opts ) {
		if (!opts) opts = {};
		// enabling _either setHash or pollHash will also enable the other, unless specifically disabled
		if (opts.setHash && opts.pollHash !== false)		opts.pollHash = true;
		else if (opts.pollHash && opts.setHash !== false)	opts.setHash = true;

		var _ = Tool
		,	o = $.extend(_.options, _.default_options, opts)
		;

		// TODO: add language info to Javascript (page_header) and read it from there
		if (!o.language || !_text[ o.language ])
			o.language = _.default_language;
		_.language = Tool.text[ o.language ]; // create a pointer to correct language subkey
		// allow overriding/customing standard language text
		if (o.text && is('Object', o.text))
			$.extend( _.language, o.text );

		if (_.options.pollHash) Tool.initHashPolling();

		_.initialized = true;
	}

,	focusFirstFormFieldOnLoad: function () {
		// called as custom_js, eg: custom_js: 'Tool.focusFirstFormFieldOnLoad()'
		$(document).ready(function(){
			$(Tool.options.formSelector +' :input').eq(0).focus();
		});
	}

,	initHashPolling: function () {
		window.timerHashPoll = setInterval( Tool.pollHash, 1000 );
	}

,	pollHash: function () {
		// if processing in progress, then abort
		if (window.timerToolProcess) return;
		var
			_	= Tool
		,	o	= _.options
		,	hash = window.location.hash
		;
		if (!hash || !hash.match(/^#!/))
			return;

		hash = hash.split("#!")[1];
		var uriLast = decodeURIComponent(_.lastHashValue);
		// NOTE: last criteria here is to catch line-breaks (auto-encoded)
		if (hash === _.lastHashValue || hash === uriLast || decodeURIComponent(hash) === uriLast)
			return;

		_.lastHashValue = hash;

		if (o.pollCallback)
			o.pollCallback.call(this);
		else {
			// TODO: populate fields as needed, THEN...
			var	$form = $(o.formSelector)
			,	pairs = hash.split('&')
			,	type, vals, regx
			;
			if (!$form.length) return; // ERROR - can't find the form!
			// unmark ALL checkboxes to start...
			$form.find('input[type=checkbox]').prop('checked', false);
			// now process the hash and set corresponding form-field values
			for (var i=0, c=pairs.length; i<c; i++) {
				pair = pairs[i].split('='); // "key=value" => ["key","value"]
				$field = []; // in case next line *fails*
				$field = $form.find(':input[name="'+ decodeURIComponent( pair[0] ) +'"]');
				if (pair.length === 2 && $field.length) {
					value	= decodeURIComponent( pair[1] );
					type	= $field.attr('type');
					if (type && type.match(/(checkbox|radio)/)) {
						if ($field.length > 1 && value.length && value !== 0 && value !==1) {
							// may be a comma-delimited list of values - match against field.values
							$field.each(function(){
								regx = new RegExp('\\b'+ this.value +'\\b')
								if (value.match( regx ))
									this.checked = true;
							});
						}
						else
							$field.prop('checked', !!value);
					}
					else
						$field.val( value );
				}
			};
			// now trigger a normal form.submit()
			$form.find('button.submit-tool').trigger('click');
		}
	}

,	setHash: function ( formData, FORM ) {
		var _		= Tool
		,	o		= _.options
		,	$Form	= FORM ? $(FORM) : $(o.formSelector)
		,	fields	= o.hashFields
		,	hash	= '' // init
		,	done	= {}
		,	name, value
		;
		if (!$Form.length) return;

		// only process 'visible' fields
		$Form.eq(0).find(':input:visible').each(function(){
			name = this.name
			if (fields && !fields.match(new RegExp('\\b'+ name +'\\b')))
				return;; // SKIP field - not specified in hashFields list
			if (name.length && name !== 'ajax' && !done[name] && (value = formData[name]) != undefined && value.length) {
				hash += '&'+ encodeURIComponent( name ) +'='+ encodeURIComponent( value );
				done[name] = 1; // avoid adding fields with same name multiple time, ie, a 'group' of radio-buttons or checkboxes
			}
		});

		if (hash) {
			_.lastHashValue = hash.substr(1); // strip first "&"
			window.location.hash = '#!'+ _.lastHashValue; // ERROR: changes %20 => " ", and possibly other decoding!
			// NOTE: if there are search/GET params on URL, then end up with TWO sets of params! (search + hash)
			//alert( 'setHash() \n\n_.lastHashValue = '+ _.lastHashValue );
		}
	}

,	showResultWrapper: function ( mode ) {
		var _ = Tool;
		$(_.el_result_header).css('display', mode ? 'block' : 'none');
		$(_.el_result_wrapper).css('display', mode ? 'block' : 'none');
	}

,	showLoadingEl: function ( mode ) { $(Tool.el_result_wrapper +' '+ Tool.el_result_loading).css('display', mode ? 'block' : 'none'); }
,	showOutputEl: function ( mode ) { $(Tool.el_result_wrapper +' '+ Tool.el_result_output).css('display', mode ? 'block' : 'none'); }
,	showErrorEl: function ( mode ) { $(Tool.el_result_wrapper +' '+ Tool.el_result_error).css('display', mode ? 'block' : 'none'); }

,	loadResult: function ( data, textStatus ) {
		var _ = Tool;
		$(_.el_result_wrapper +' '+ _.el_result_output).html( data );
		_.showResult();
	}

,	hideResult: function () { Tool.showResultWrapper(0); }

,	showLoading: function () {
		var _ = Tool;
		_.showOutputEl(0);
		_.showErrorEl(0);
		_.showLoadingEl(1);
		_.showResultWrapper(1);
	}

,	showResult: function () {
		var _ = Tool;
		//_.showErrorEl(0); // DO NOT hide error because this may _be_ the result!
		_.showLoadingEl(0);
		_.showOutputEl(1);
		_.showResultWrapper(1);
	}

,	showError: function () {
		var _ = Tool;
		_.showLoadingEl(0);
		_.showOutputEl(0);
		_.showErrorEl(1);
		_.showResultWrapper(1);
	}

,	submitForm: function ( opts, noAjax ) {
		var _ = Tool;
		if ( _.ajaxSubmitXHR ) {
			try { _.ajaxSubmitXHR.abort(); _.ajaxSubmitXHR = null; } catch (ex) {};
			//alert( 'A previous attempt is still being processed.\n\nIf there is no response, refresh this page and try again.' );
			//return false;
		}

		if (opts) _.options.required = opts;
		else opts = {};

		var	tag		= this.tagName
		,	$Form	= opts.form ? $(opts.form) : tag==='FORM' ? $(this) : tag ? $(this).closest('FORM') : $('form.tool-options')
		,	$Inputs = $Form.find(':input') // find ALL input fields in Form
		,	$Output	= $Form.siblings('div.result-wrap:first')
		,	o		= _.options
		,	required = o.required
		,	arr_fields, key, data, name, vals, check, i, c
		;

		// if simple delimited string passed, then convert to a hash
		if (is('String', required)) { // eg: "domain,keyword"
			arr_fields	= required.split(',');
			required	= o.required = {}; // create empty hash - then populate...
			for (i=0, c=arr_fields.length; i<c; i++) {
				name = arr_fields[i];
				required[ name ] = '';
			}
		}

		// options.required _must_ be a hash/JSON by now
		// SAMPLE: { domain:"Domain Name", keyword:"", engines:{ display:"Search Engine", require:1, fields:"google,yahoo,bing,ask" } }
		//		fieldname: value => "display" _or_ "" = find LABEL || hash = data: display (str), fields (comma-delim), require (int)
		if (is('Object', required)) {
			for (key in required) {
				data = required[key];
				if (is('String', data)) { // eg: fieldname: 'Display Name'
					if (checkFieldValue( key, data, true ) < 1)
						return false; // abort submit
				}
				else if (is('Object', data) && data.fields) { // JSON - probably a 'group' of fields
					vals	= 0;	// number of fields in group 'with values'
					blank	= '';	// FIRST blank field found
					arr_fields = data.fields.split(',');
					for (i=0, c=arr_fields.length; i<c; i++) {
						check = checkFieldValue( arr_fields[i] );
						if (check > 0)
							vals += check; // ADD check because if multiple checkboxes found, check = 'number' that were checked
						else if (!blank && check !== -1) // -1 = field is MISSING!
							blank = arr_fields[i];
					}
					// TODO: add other forms of validation here if/as required
					// if field-group does not meet requirement, FAIL now
					if (data.require && vals < data.require) {
						// call again - but show error msg this time!
						checkFieldValue( blank, data.display, true );
						return false; // abort submit
					}
				}
			}
		}

		window.timerToolProcess = Date.now();

		// clear default values in preparation for submit
		$Form.find('input').each( clearDefaultValue );

		// if noAjax option is set, then just allow the form to submit normally
		if (noAjax) return true;

		// create hash of ALL form fields
		var formData = { ajax: true };
		$Inputs.each(function(){ // exclude un-checked checkboxes & radio-buttons
			var name = this.name || '';
			if (name && ( !this.type || !this.type.match(/(checkbox|radio)/i) || this.checked )) {
				if (formData[name]) // this field already exists!
					formData[name] += ','+ this.value; // create comma-delimited list of values
				else
					formData[name] = this.value;
			}
			
		});

		// if a dataProcessor function is set, it will return a new or modified data hash
		if (o.dataProcessor)
			postData = o.dataProcessor( $.extend({}, formData) ); // COPY formData so original is not altered
		else
			postData = formData;

		// show the loading message
		_.showLoading();
		// use Ajax to submit form...
		_.ajaxSubmitXHR = $.get("process_tool.php", postData, function( html, textStatus ) {
			var _ = Tool;
			_.loadResult( html );
			_.ajaxSubmitXHR = null;			// CLEAR for logic
			window.timerToolProcess = null; // CLEAR processing flag
			if (o.onResultLoad)				// post-injection processing, eg: event-binding on inserted element(s)
				o.onResultLoad(formData);
			if (o.setHash)					// update the URL hash
				_.setHash( formData, $Form );
		});

		return false; // cancel normal submit


		// SUBROUTINE - test for a field value OR if 'checked'
		function checkFieldValue (field, display, alertUser) {
			var $E	= $Inputs.filter('[name="'+ field +'"]')
			,	exists	= $E.length
			,	multi	= exists && $E.length > 1 // true = more than 1 field with this name
			,	type	= exists ? $E[0].type : ''
			,	retVal	= 1
			;
			if (!exists) {
				if (!alertUser) return -1; // -1 = 'missing field'
				else val = false;
			}
			else if (multi) { // probably a 'group' of checkboxes/radios
				retVal = 0; // retVal = 'count' of fields that have values/are checked
				$E.each(function(){
					if (type.match(/(checkbox|radio)/i))
						retVal += this.checked ? 1 : 0;
					else
						retVal += $.trim(this.value).length ? 1 : 0;
				});
				val = retVal || false;
			}
			else {
				val	= type.match(/(checkbox|radio)/i) ? $E.is(':checked') : $.trim($E.val());
			}
			if (val === false || val === '' || val === $E.attr('alt')) { // 'alt' = 'sample value'
				if (alertUser) {
					if (exists) $E.focus();
					alert( 'Enter the '+ (display || (exists ? Content.getFieldLabel($E) : 'criteria')) +' you want to test.' );
				}
				return 0; // field has NO value, or is un-checked
			}
			return retVal; // field HAS value, or is checked
		};
	}

,	createCommaDelimitedList: function (field_or_value, opts) {
		// strip invalid chars and replace line-breaks with commas
		var	isField	= $.type(field_or_value) === 'object'
		,	value	= isField ? field_or_value.value : field_or_value
		,	a_vals	= value
			.toLowerCase()
			.replace(/[!%\'"()]/g, "")
			.replace(/\n/g, "~")
			.replace(/\s/g, "~")
			.replace(/,/g, "~")
			.replace(/~{2,}/g, "~")
			.replace(/(.*?)\..*?~/g, "$1~")
			.replace(/(.*)\..*?$/g, "$1")
			.split("~")
		;
		// not sure what this is for? Removing blank entries maybe?
		a_vals = $.grep(a_vals, function(n, i) { return(n); });
		//debugData( a_vals );
		// see if we need to limit/reduce the number of items
		if (opts.limit > 0)
			a_vals = a_vals.splice(0, opts.limit);
		// now convert array back to a string
		newVal = a_vals.join(',');
		// update the form-field with new value
		if (opts && opts.updateField && isField)
			field_or_value.value = newVal;
		// update the cleaned value
		return newVal;
	}

};


/*
 *	User login/logout and similar handling
 */
var User = {
//	DROP-DOWN Login - from top-user-bar
	loginDropdownId:	'ajaxLoginForm'
,	loginDropdownPath:	'/bin/topbar_login.php'	// drop-down login-box from link in top-user-bar
,	loginPlaceholderId:	'ajaxLoginLoading'
,	loginOpenClass:		'menuOpen'	// applied to login button when Login Popup is open
,	loginLineSelector:	'#topuserpanel a.login'

//	POPUP Login - loads inside UI Dialog OR inside any element/area desired
,	loginPopupId:		'loginform'
,	loginPopupPath:		'/bin/popup_login.php'

//	AJAX Login Processing - returns a standard data-array
,	loginAjaxPath:		 '/bin/login.php'	// used for Ajax logins

//	NORMAL Login - non-Javascript login
,	loginPagePath:		'/account/login/'	// normal Login Page - also used for non-Ajax login-submit

,	init: function () {
		//$( User.loginLineSelector ).click( User.toggleLoginDropdown ); - this is hard-coded on the button
	}

,	getLanguageText: function (code, type, replacements) {
		var text = '';
		$.ajax({
			data: {
				msg_code:		code
			,	msg_type:		type || 'messages'	// messages type is default
			,	replacements:	replacements		// must be a JSON array to be valid
			}
		,	url:		'/bin/get_language_text.php'
		,	type:		'POST'
		,	xhrFields:	{ withCredentials: true }
		,	dataType:	'html'	// what is returned
		,	async:		false	// NOT asynchronous - calling methods need text back to display NOW
		,	timeout:	10000
		,	success:	function (data, status)	{ text = data; }
		,	error:		function (xhr, error)	{ text = ''; }
		});
		return text;
	}


,	toggleLoginDropdown: function ( mode ) {
		var _	= User
		,	$P	= $('#'+ _.loginDropdownId)
		,	$A	= $( User.loginLineSelector )
		,	vis	= $P.is(':visible')
		;
		if ($P.length && (mode === false || vis)) {
			$P.hide();
			$A.removeClass( _.loginOpenClass )
		}
		else if (mode === true || !vis) {
			if ($P.length)
				openPopup();
			else {
				// show the 'loading' placeholder...
				$P = $('#'+ _.loginPlaceholderId);
				openPopup();
				// now get the REAL login-box
				$.ajax({
					url:		_.loginDropdownPath
				,	timeout:	10000	
				,	success: function(html) {
						$('#page_popups').append( html );
						$P.remove(); // DELETE the placeholder - don't need it anymore
						$P = $('#'+ _.loginDropdownId);
						openPopup();
					}
				});
			}
		}
		return false; // cancel hyperlink
		
		function openPopup () {
			$P.show().position({
				my:		'left top'
			,	at:		'left bottom'
			,	of:		$A
			,	collision: 'none' // flip fit none
			,	offset:	'0 3'
			});
			//	FIX: this hack is because top position is 3px low when on the login page - don't know why?!
			$P.css('top', $('#topuserpanel').outerHeight());
			$A.addClass( _.loginOpenClass );
			$('#login_username_topbar').focus(); // move focus to username field
		}
	}


,	closeLoginPopup: function ( showMsg ) {
		var $form = $('#'+ User.loginPopupId);
		if ($form.length && $form.is(':visible')) {
			$form.dialog('close') // close the dialog
				.find('#login_password').val(''); // clear password field, just in case
			if (showMsg)
				;//alert('something!?');
		}
	}

,	openLoginPopup: function ( Args ) {
		/* Possible Args keys...
		Args = { msgCode, msgType, redirect, callback }
		*/
		if (!Args) Args = {};
		var $form = $('#'+ User.loginPopupId);
		if (!$form.length) {
			if (Args.msgCode === undefined) {
				Args.msgCode = 'session_timeout'; // allow for "blank" msgCode to be passed == No Msg
				Args.msgType = 'warning';
			}
			else if (!Args.msgType)
				Args.msgType = 'warning';
			var url = User.loginPopupPath +( Args.msgCode ? ('?'+ Args.msgType +'='+ Args.msgCode) : '');
			$.ajax({
				url:		url
			,	timeout:	10000	
			,	success: function (data, status) {
					if (status === 'success' && data.match(/id="loginform"/)) {
						$('#page_popups').append( data );
						User.openLoginPopup( Args );
					}
					else // unable to load the login popup!?
						self.location = User.loginPagePath;
				}
			});
		}
		else {
			$form.children('input[name=ajax]').val(1); // since opened via JS, will submit via Ajax
			if ( Args.redirect )
				$form.children('input[name=login_redirect]').val( Args.redirect );
			else if ( Args.callback ) {
				$form.children('input[name=login_redirect]').val('no'); // PREVENT any login redirection
				// cache callback in global scope so ajaxLogin can find it - exists only until page is refreshed
				window.loginCallback = Args.callback;
			}
			//alert( 'redirect = '+ $form.children('input[name=login_redirect]').val() ); // TESTING
			$form.dialog({
				title:		'Member Login'
			,	width:		'70ex'
			//,	height:		300
			,	autoOpen:	true
			,	modal:		true
			,	resizable:	false
			,	closeOnEsc:	false
			//,	close:		function() { self.location.reload(); }
			/*
			,	buttons: {
					Login: function() { $(this).dialog('close'); }
				,	Cancel: function() { $(this).dialog('close'); }
				}
			*/
			});
		}
		return false; // cancel hyperlink
	}

	/*
	 * NOTE: this code handles the Login box in the Topbar AND the one in the Sidebar
	 */
,	ajaxLogin: function (FORM, opts) {
		// if calling login-form is on the actual Login page, then let it submit normally
		if (self.location.pathname == User.loginPagePath) return true;

		if (!opts) opts = {};

		var
			F	= FORM.elements		|| {}
		,	U	= F.login_username	|| {}
		,	P	= F.login_password	|| {}
		,	R	= F.login_remember	|| {}
		,	X	= F.login_redirect	|| {}
		//,	B	= F.login - not used
		,	$F	= $(FORM)
		,	$W 	= $F.parent() // $('#ajax'+ User.loginPopupId) = Topbar Login-box Wrapper
		//,	$W 	= parent.$('#ajax'+ User.loginPopupId) // IFRAME VERSION - Login-box Wrapper on parent-page
		;
		/* OLD
		// if missing a username or password, return true to send user to the Login page
		if (!U || !U.value || !P || !P.value)
			return true;
		*/
		/*
		debugData({
			login_username:	U.value
		,	login_password: P.value
		,	remember_me:	(R.checked ? 'on' : '')
		,	redirect_url:	window.location.href
		}, 'Login Form Data' );
		return false;
		*/

		// SHOW Processing - both mask and message are DIVs that are siblings of FORM...
		$W.children('div').show();
		$F.find(':input').prop('disabled', true); // disable form fields

		var
			path	= User.loginAjaxPath
		,	host	= top.location.host
		//	use https protocol ONLY for live server, else use local path
		,	prefix	= host.match(/^(www|secure)\.webmasterbond\.com$/) ? 'https://'+ host : ''
		;
		$.ajax({
			data: { // login data
				ajax:			true
			,	login_username:	U.value
			,	login_password: P.value
			,	remember_me:	(R.checked ? 'on' : '')
			,	redirect_url:	X.value || top.location.href
			//,	javascript_off:	'yes'	// DEPRICATED - prevent login/process.php from outputting a script tag!
			}
		,	url:		prefix + path
		,	type:		"POST"
		,	crossDomain: true
		,	xhrFields:	{ withCredentials: true }
		,	dataType:	'json' // what is returned
		,	timeout:	10000
		,	success: function (data, status, xhr) {
				// HIDE Processing mask & message
				$W.children('div').hide();
				$F.find(':input').prop('disabled', false); // re-enable form fields

				if (!$.isPlainObject( data )) // INVALID response - should be JSON
					this.error(xhr, status, 'unexpected data-type'); // treat as unknown error
				else if (!data.success) { // failed - give an error msg
					if (data.errors_text)
						alert( data.errors_text );
					else if (data.errors) // should ALWAYS be an error-msg returned
						alert( User.getLanguageText( data.errors ) );
					else
						alert( 'Unable to login.' );
				}
				else { // login SUCCESS
					// close the drop-down or popup
					if (FORM.id === User.loginDropdownId)
						User.toggleLoginDropdown(false);
					else if (FORM.id === User.loginPopupId)
						User.closeLoginPopup();

					// see if we were instructed to do a callback or reload the page
					if (window.loginCallback && $.isFunction( window.loginCallback )) {
						//alert( window.loginCallback ); // DEBUG
						window.loginCallback();
						delete window.loginCallback;
					}
					/* TESTING
					else if (data.loadElem && data.loadUrl)
						$( data.loadElem ).load( data.loadUrl ); // CONTINUE WITH INTERRUPTED AJAX LOAD
					*/
					else if (!opts.noReload)
						window.location.reload();
				}

				// return JSON data to calling method
				return data;
			}
		,	error: function (xhr, status, error) {
				if (!opts.noAlerts)
					alert( "Login failed due to a '"+ error +"'" );
				// send user to login page so can login there
				window.location = User.loginPagePath;
				//this.complete();
			}
		,	complete: function (xhr, status) {}
		});

		return false; // cancel normal submit
	}

	// checked Session to see if logged-in, and returns true or false accordingly
,	isLoggedIn: function () {
		return $.ajax({
			url:		'/bin/is_logged_in.php'
		,	type:		"GET"
		,	async:		false // must be SYNCHRONOUS so can pass back an answer to calling function
		,	crossDomain: true
		,	xhrFields:	{ withCredentials: true }
		,	dataType:	'text' // what is returned
		,	timeout:	10000
		}).responseText == 'true' ? true : false;
	}

	// if user is NOT logged-in, open the Popup Login box
	// returns false is not _currently_ logged in to abort calling method
	// pass Args.callback to re-trigger calling method IF user logs in successfully
,	validateIsLoggedIn: function ( Args ) {
		if (!$.url.data) $.url.parse(); // if an Ajax page calls this method, need to create data for that
		var d = $.url.data;
		// if URL has an ID and Password, allow use to continue - let backend validation handle it
		if ( d.id && d.id > 0 && d.pass && d.pass.length === 32 )
			return true; 
		if ( User.isLoggedIn() )
			return true; 
		else {
			// Args probably contains a 'callback' function, and maybe 'msgType' & 'msgCode'
			User.openLoginPopup( Args );
			return false;
		}
	}

};


/*
 *	Collapsible sections under a heading
 */
var Collapsible = {
	options: {
		animate:		true
	,	animateSpeed:	'normal'
	,	tipText:		'click to ACTION'
	,	initCollapsed:	false
	}

,	initOnLoad: function () {
		$(document).ready( Collapsible.initByHeading );
	}

,	initByHeading: function (opts) {
		var	_	= Collapsible
		,	$E	= $(this)
		,	$H	= $E.prev()
		,	vis	= $E.is(':visible')
		;
		if (typeof opts !== 'object') opts = {};
		if ($H.length && !$H.data('collapsible')) {
			$H	.data('collapsible', $.extend( {}, _.options, opts ))
				.click( Collapsible.toggleNext )
				.addClass('collapsible-heading')
				.prepend('<a class="tip" href="#" onclick="this.blur(); return false"></a>')
			;
			if ($H.data('collapsible').initCollapsed && vis)
				_.toggleNext.call($H, true); // collapse now if initCollapsed==true
			else
				_.addTip($H, !vis);
		}
	}

,	initContentWrapper: function () {/* TODO: may need to dynamically wrap 'multiple P elems' in a DIV */}

,	toggleNext: function (noAnimate) {
		var	_	= Collapsible
		,	$H	= $(this)
		,	$E	= $H.next()
		,	o	= $H.data('collapsible')
		,	vis	= !$E.is(':visible') // visibility AFTER toggling
		;
		if (o.animate && noAnimate !== true) {
			//$Block.stop(true,true).toggle('slow', fixOpacity);
			if (vis) _.addTip($H, !vis); // better look
			$E.stop(true,true).slideToggle(_.animateSpeed, function(){
				fixOpacity.call(this);
				if (!vis) _.addTip($H, !vis);
			});
		}
		else { // no animation
			_.addTip($H, !vis);
			$E.toggle();
		}
	}

,	addTip: function ($H, isCollapsed) {
		var tip = $H.data('collapsible').tipText.replace(/ACTION/g, ( isCollapsed ? 'show' : 'hide'));
		$H.attr('title', tip).children('a.tip').html( tip );
		if (isCollapsed)
			$H.addClass('collapsible-collapsed')
		else
			$H.removeClass('collapsible-collapsed')
	}
};


/*
 *	Methods used for navigation, including popup menus
 */
var Nav = {
	options:	{}
,	default_options: {
		floatCrumbbar:		true
	,	floatSidebar:		true
	}

,	state: {
		supportsFixedPosition:	!$.browser.msie || $.browser.version >= 7
	,	isCrumbbarEnabled:		false
	,	isCrumbbarFloating:		false
	,	isSidebarEnabled:		false
	,	isSidebarFloating:		false
	}

,	init: function (opts) {
		var
			_	= Nav
		,	o	= _.options = $.extend( true, _.default_options, opts || {} )
		,	s	= _.state
		;

		// initialize floating breadcrumb-bar
		if (s.supportsFixedPosition) {
			if (o.floatCrumbbar)
				_.enableFloatingCrumbbar();
			if (o.floatSidebar)
				_.enableFloatingSidebar();
		}

		// initialize WordPress sidebar navigation
		_.initWordPressNav();

		var Nav1 = new Menuset({
			ajaxParam:		'menus=nav1'
		,	menusLocation:	'#topnavbar'
		,	menusWrapper:	'#nav1_menus'
		,	menusSelector:	'.nav1-menu'
		,	linksWrapper:	'#nav1'
		,	linksSelector:	'a'		// ALL nav1 links have menus
		,	alignMode:		'center' // center menus under tab
		,	offsetTop:		-5		// counter -5px A.margin-bottom onHover (to mask menu.border-top)
		,	offsetLeft:		0
		});
		// preload menus - after a delay
		setTimeout( $.proxy( Nav1, 'loadMenus' ), 3000 );

		var Nav3 = new Menuset({
			menusLocation:	'#crumbbar_wrapper'
		,	menusWrapper:	'#nav3_menus'
		,	menusSelector:	'.nav3-menu'
		,	linksWrapper:	'#breadcrumb'
		,	linksSelector:	'a.menu'
		,	alignMode:		'left'
		,	offsetTop:		-3
		});
		// preload menus - after a delay
		setTimeout( $.proxy( Nav3, 'loadMenus' ), 4000 );

		// bind event to primary Search-box
		$('#nav1 > .search input:first').keyup( clearValueOnESC );

	}

, 	initWordPressNav: function () {
		/*
		 *	cssMenu for drop-down menus
		 */
		var $menu = $('#wp-navmenu.widget_mypageorder');
		if ( !$menu.length ) return; // No WP sidebar-menu on this page
		// after a WP update, there is now a DIV inside the UL (which is actually invalid mark-up)
		if ($menu.find('> ul > div.menu').length)
			$menu = $menu.find('> ul > div.menu');

		// add class & span used for 'arrows' and JS functionality
		$menu.find('ul.children').each(function(){
			$(this).parent() // <LI>
				.addClass('submenu')
				.children('a')
					.attr('title','')			// tooltips interfer with flyout menu hovering, so nuke em!
					.append('<span></span>')	// add a span for menu arrows
		});
		// add 'black' hyperlink class to TOP_LEVEL (visible) items
		$menu.find('> ul > li > a').addClass('black');

		// enable Javascript enhancements
		$menu.children('ul').cssMenu({
			animateOpen:	true
		,	animateSpeed:	'normal'
		,	closeMenuDelay:	500
		//,	exclude:		''
		});

		/* DEBUG
		alert(
			'$menu.length = '+		$menu.length
		+'\n li.submenu > a = '+	$menu.find('li.submenu > a').length
		+'\n ul.children = '+		$menu.find('ul.children').length
		+'\n $menu > ul = '+		$menu.children('ul').length
		);
		*/
	}

,	enableFloatingCrumbbar: function (mode) {
		var _ = Nav
		,	o = _.options
		,	s = _.state;
		if (mode === false && s.isCrumbbarEnabled) {
			o.floatCrumbbar		= false;
			s.isCrumbbarEnabled	= false;
			$(window).unbind( 'scroll.crumbbar' );
			if (s.isCrumbbarFloating)
				_.unfloatCrumbbar();
		}
		else if (mode !== false && !s.isCrumbbarEnabled && $('#crumbbar_placeholder').length) {
			o.floatCrumbbar		= true;
			s.isCrumbbarEnabled	= true;
			$(window).bind( 'scroll.crumbbar', Nav.testCrumbbarPosition );
			_.testCrumbbarPosition();
		}
	}
,	testCrumbbarPosition: function () {
		var _	= Nav
		,	o	= _.options
		,	s	= _.state
		,	offScreen = $(window).scrollTop() >= $('#crumbbar_placeholder').offset().top
		;
		if (offScreen && !s.isCrumbbarFloating)
			_.floatCrumbbar()
		else if (!offScreen && s.isCrumbbarFloating)
			_.unfloatCrumbbar()
	}
,	floatCrumbbar: function () {
		var _ = Nav
		,	o = _.options
		,	s = _.state;
		if (!o.floatCrumbbar || s.isCrumbbarFloating) return;
		s.isCrumbbarFloating = true;
		var $Wrap = $('#crumbbar_wrapper');
		$('#crumbbar_placeholder').height( $Wrap.outerHeight() );
		$Wrap.addClass('fixed');
	}
,	unfloatCrumbbar: function () {
		var _ = Nav
		,	o = _.options
		,	s = _.state;
		if (!s.isCrumbbarFloating) return;
		s.isCrumbbarFloating = false;
		$('#crumbbar_wrapper').removeClass('fixed');
		$('#crumbbar_placeholder').height('auto');
	}


,	enableFloatingSidebar: function (mode) {
		var _	= Nav
		,	o	= _.options
		,	s	= _.state
		,	$E	= $('div.non_scrolling_sidebar')
		//,	bottomLimit = $E.data('floatingBottomLimit') why not working???
		,	bottomLimit = $E.attr("data-floatingBottomLimit")
		;
		if (!$E.length || !$E.closest('.sidebar').length)
			return;

		if (bottomLimit && $(bottomLimit).length)
			_.options.$bottomFloatLimitEl = $(bottomLimit);

		if (mode === false && s.isSidebarEnabled) {
			o.floatSidebar		= false;
			s.isSidebarEnabled	= false;
			$(window).unbind( 'scroll.sidebar' );
			if (s.isSidebarFloating)
				_.unfloatSidebar();
		}
		else if (mode !== false && !s.isSidebarEnabled) {
			o.floatSidebar		= true;
			s.isSidebarEnabled	= true;
			$(window).bind( 'scroll.sidebar', Nav.testSidebarPosition );
			_.testSidebarPosition();
		}
	}
,	testSidebarPosition: function () {
		var _			= Nav
		,	o			= _.options
		,	s			= _.state
		,	offset		= 10 // normal offsetTop (whitespace between screen-top and floater)
		,	$E			= $('div.non_scrolling_sidebar')
		,	elTop		= $E.css('top')
		,	elHeight	= $E.outerHeight() - parseInt($E.children(':last').css('marginBottom'), 10)
		,	sidebarTop	= $E.closest('.sidebar').offset().top
		,	$B			= o.$bottomFloatLimitEl
		,	maxBottom 	= $B ? $B.offset().top + $B.outerHeight() : 0
		,	scrollTop	= $(window).scrollTop()
		,	cbHeight	= o.floatCrumbbar ? $('#crumbbar_wrapper').outerHeight() : 0
		,	offScreen	= (scrollTop + cbHeight + offset) >= sidebarTop
		,	fixedTop	= cbHeight + offset
		,	roomToFloat, belowMaxBottom
		;
		if (maxBottom) {
			if (s.isSidebarFloating) {
				belowMaxBottom	= (scrollTop + elTop + elHeight) > maxBottom;
				roomToFloat		= (elTop + elHeight) > maxBottom;
				if (belowMaxBottom && (elTop + elHeight) < maxBottom)
				//if ((elTop + elHeight) < maxBottom)
					// apply NEGATIVE fixed-position so floater bottom will align with maxBottom
					fixedTop = (maxBottom - scrollTop) - elHeight;
				else
					offScreen = false; // UN-FLOAT the elem
			}
			else if (!s.isSidebarFloating) {
				roomToFloat		= (elTop + elHeight) > maxBottom;
			}
			
/*
			// floater is BELOW maxBottom, so either raise or unfloat it
			if (!s.isSidebarFloating && (scrollTop + cbHeight + offset + elHeight) > maxBottom)
				return; // NOT floating yet, so just abort
			if (s.isSidebarFloating && (elTop + elHeight) < maxBottom)
				// apply NEGATIVE fixed-position so floater bottom will align with maxBottom
				fixedTop = (maxBottom - scrollTop) - elHeight;
			else
				offScreen = false; // UN-FLOAT the elem
 */
		}

console.log(debugData({
	maxBottom:		maxBottom
,	scrollTop:		scrollTop
,	cbHeight:		cbHeight
,	offset:			offset
,	elHeight:		elHeight
,	fixedTop:		fixedTop
,	test:			((scrollTop + cbHeight + offset + elHeight) > maxBottom)
},	'testSidebarPosition',	{ display: false, returnHTML: false, sort: false } ));

		if (offScreen && s.isSidebarFloating && fixedTop != elTop) {
			// sidebar already floating - just update its 'top' position
			$E.css('top', fixedTop);
		}
		else if (offScreen && !s.isSidebarFloating) {
			//_.floatSidebar()
			s.isSidebarFloating = true;
			$E.css('top', fixedTop).addClass('fixed');
		}
		else if (!offScreen && s.isSidebarFloating) {
			//_.unfloatSidebar()
			s.isSidebarFloating = false;
			$E.css('top', 0).removeClass('fixed');
		}
	}
,	floatSidebar: function () {
		/* UNUSED - CODE INCORPORATED IN testSidebarPosition() */
		var _	= Nav
		,	o	= _.options
		,	s	= _.state
		,	$E	= $('div.non_scrolling_sidebar')
		,	cb	= o.floatCrumbbar ? $('#crumbbar_wrapper').outerHeight() : 0
		;
		if (!o.floatSidebar || s.isSidebarFloating) return;
		s.isSidebarFloating = true;
		$('div.non_scrolling_sidebar')
			.addClass('fixed')
			.css('top', 10 + cb);
	}
,	unfloatSidebar: function () {
		var _	= Nav
		,	o	= _.options
		,	s	= _.state
		,	$E	= $('div.non_scrolling_sidebar')
		;
		if (!s.isSidebarFloating) return;
		s.isSidebarFloating = false;
		$E.removeClass('fixed').css('top', 0);
	}

};

/**
 *	Nav-Menu object
 *
 *	Code for navbars with drop-down menus
 *	currently the Nav1 and Breadcrumbs use this
 */
var Menuset = function ( opts ) {
	this.options = {
		ajaxParam:		'path='+ encodeURIComponent( self.location.pathname + self.location.search + self.location.hash )
	,	menusLocation:	'body'
	,	menusWrapper:	''
	,	menusSelector:	''
	,	linksWrapper:	''
	,	linksSelector:	'a.menu'
	,	pollInterval:	100 	// ms (100) - how often should mouse-position be checked for accelleration (hoverIntent functionality)
	,	sensitivity:	7		// px (7)	- mouse must move LESS than this within pollInterval ms to trigger open (accelleration)
	,	openMenuDelay:	250		// ms (500) - NOTE: <500 because mouse must 'decellerate' before open-delay timer even starts
	,	closeMenuDelay:	400		// ms (500) - MUST be minumum of 50 so mouse can pass from link to menu, and back, without menu closing
	,	animateSpeed:	200 	// ms (500) - delay + 200 animate = 700ms TOTAL to display/hide menus
	,	animateOpen:	true
	,	alignMode:		'left'	// left | right | center -> will align left or right IF trigger close to side - see minMenuShift
	,	alwaysPosition:	false	// true = reposition menu 'invisibly' EVERY TIME the menu opens / false = check position AFTER opening
	,	testMouseOver:	false	// true = use an extra test to check when mouse is no longer over a menu
	,	minMenuShift:	20		// when less than this from side-of-content, align to left/right of link/tab instead of trying to center
	,	offsetTop:		0		// menu offset adjustment - usually negative, eg: -3
	,	offsetLeft:		0		// ditto
	};
	this.state = {
		menusLoaded:	false	// have menus been loaded via Ajax yet?
	,	openMenuIndex:	-1		// currently open menu index among siblings
	, 	openMenuId:		''		// currently open menu ID
	,	nextMenuIndex:	-1		// next menu to open - unless user mouses-out before timer expires
	};
	this.timers = {
		open:	null	// delayed menu-open
	,	close:	null	// delayed menu-close
	,	poll:	null	// check mouse position to test decelleration (hoverIntent)
	,	test:	null	// check to see if mouse is STILL over an element - TODO: can this be phased out?
	};
	this.init( opts ); // automatic initialization
};
Menuset.prototype = {

	clearTimer: function (name) {
		var t = this.timers[ name ];
		if (t) { clearTimeout( t ); t = null; }
	}

,	init: function ( opts ) {
		var _	= this
		,	o	= _.options
		;
		if (opts) $.extend( o, opts );

		if (o.testMouseOver)
			$.mousePosition.init(); // in case not already

		// bind events to trigger-links
		$( o.linksWrapper ).find( o.linksSelector )
			.hover(
				$.proxy( this, 'enterLink' )
			,	$.proxy( this, 'leaveLink' )
			)
			// make drop-down-arrow clickable
			.children('i').click( /* link arrow */
				$.proxy( this, 'toggle' )
			)
		;
	}

	// init the drop-down menus on the Crumbbar-path elems
,	loadMenus: function ( el_trigger, force ) {
		var _	= this
		,	o	= _.options
		,	s	= _.state
		;
		if (s.menusLoaded === true) // true = 'loaded' (TRUE) OR 'loading' (-1)
			return true;
		// SKIP loading if another Ajax process is currently running - UNLESS force == true
		if (!force && ($.active || $.ajax.active)) // $.ajax.active is new syntax in next version
			return false;
		// set or update the nextMenuIdx - will open this menu as soon as Ajax completes
		if (el_trigger) s.nextMenuIndex = $(el_trigger).index();
		// if menu-loading hasn't started yet, then do it now...
		if (s.menusLoaded === false) { // false = 'not loaded' / -1 = 'loading in progress'
			s.menusLoaded	= -1; // -1 = starting to load menus - not complete yet
			// set global var used by SEO app and maybe elsewhere
			window.backgroundAjaxRequest = true;
			// Ajax method to load the Menus into the DOM
			$.ajax({
				url:		'/bin/get_nav_menu.php?'+ o.ajaxParam
			,	timeout:	10000	
			,	success: function (data) {
					// append menus to crumbar (hidden)
					$( o.menusLocation ).prepend( data );
					// bind mouse events to menus
					$( o.menusWrapper ).children().hover(
						$.proxy( _, 'enterMenu' )
					,	$.proxy( _, 'leaveMenu' )
					);
					s.menusLoaded = true; // Loading Complete
					delete window.backgroundAjaxRequest;
					if (s.nextMenuIndex >= 0) // now that menu is loaded, show it!
						_.show( $( o.linksWrapper ).children().get( s.nextMenuIndex ), false, true ); // pass the crumb-link element
				}
			,	error: function (xhr, status, err) {
					// PROBABLY: err = "timeout"
					s.menusLoaded = false; // Loading ABORTED
					delete window.backgroundAjaxRequest;
				}
			});
		}
		// return false to tell caller (probably show) that menus are NOT loaded yet
		return false;
	}

,	isCurrentMenu: function (el) {
		// el can be either a trigger-link or a menu
		var idx	= this.state.openMenuIndex;
		return idx >= 0 && idx === $(el).index();
	}

,	getIndex: function (el) {
		var $a	= $(el)			// el is always an A elem
		,	$li	= $a.parent();	// the A _MAY_ be inside a LI
		//	get index/position among sibling-elements - if A is _inside_ a LI, then get index of LI instead
		return $li.tagName() === 'LI' ? $li.index() : $a.index();
	}

,	getLinkByIndex: function (idx) {
		var $el = $( this.options.linksWrapper ).children();
		$el = $el.tagName() === 'UL' ? $el.children().eq( idx ) : $el.eq( idx );
		if ($el.tagName() !== 'A')
			$el = $el.find('a:first')
		return $el;
	}

	// bound to trigger-links mouseEnter
,	enterLink: function (evt) {
		var _	= this
		,	s	= _.state
		,	o	= _.options
		,	el	= evt.currentTarget
		,	idx	= _.getIndex(el)
		,	pX	= evt.pageX			// previous X and Y position of mouse, set by mouseenter and polling interval
		,	pY	= evt.pageY			// ditto
		,	cX, cY				// init current X and Y position of mouse, updated by mousemove event - track function
		,	e	= $.extend({}, evt)	// required for event object to be passed in IE - according to hoverIntent plugin
		;
		_.clearTimer('open'); // ABORT ANY OTHER MENU

		if (s.openMenuIndex === idx) // hovering the 'currently open menu' link, so...
			_.clearTimer('close'); 	// ABORT CLOSE - possibly triggered when user moused from the menu to the crumb-link

		if (s.openMenuId) {		// a menu is already open, so...
			_.show( el, true );	// OPEN menu immediately - will close any open menu OR prevent 'current menu' from closing if necessary
			return;				// DONE
		}

		// if menu hasn't loaded yet, start loading NOW - may be ready by time show() is called
		if (s.menusLoaded === false)
			_.loadMenus( null, true ); // true = force loading even if other Ajax running

		if (o.pollInterval === 0) { // no pollInterval means SKIP hoverIntent functionality
			if (o.showMenuDelay === 0)
				_.show( el, true );	// OPEN new menu immediately - will close any open menu
			else { // use ordinary 'open delay' - will be cancelled if user mouses-out before it opens
				s.nextMenuIndex = idx;
				_.timers.open = setTimeout( function(){ _.show( el ); }, o.openMenuDelay ); // SET OPEN TIMER
			}
			return;	// DONE
		}

		/**
		 *	hoverIntent functionality
		 *	Code below adapted from jquery.hoverintent.js
		 * 
		 *	hoverIntent r6 // 2011.02.26 // jQuery 1.5.1+
		 *	http://cherne.net/brian/resources/jquery.hoverIntent.html
		 *	@author	Brian Cherne brian(at)cherne(dot)net
		 */

		// A private function for getting mouse position
		var track = function (ev) {
			cX = ev.pageX;
			cY = ev.pageY;
		};
		// bind method to update "current" X and Y position based on mousemove
		$(el).bind("mousemove", track);

		// A private function for comparing current and previous mouse position
		var compare = function (e, el) {
			_.clearTimer('poll');
			// compare mouse positions to see if they've crossed the threshold
			if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < o.sensitivity ) {
				$(el).unbind('mousemove');
				s.nextMenuIndex = idx;
				_.timers.open = setTimeout( function(){ _.show( el ); }, o.openMenuDelay ); // SET OPEN TIMER
			}
			else { // mouse has not decellerated yet, so keep checking...
				pX = cX; pY = cY; // update 'previous' coordinates for next loop
				// use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
				_.timers.poll = setTimeout( function(){ compare(e, el); }, o.pollInterval );
			}
		};
		// start polling interval (self-calling timeout) to compare mouse coordinates over time
		_.timers.poll = setTimeout( function(){ compare(e, el); }, o.pollInterval );
	}

	// bound to trigger-links mouseLeave
,	leaveLink: function (evt) {
		var _	= this
		,	s	= _.state
		,	ms	= _.options.closeMenuDelay
		,	el	= evt.currentTarget
		,	idx	= _.getIndex(el)
		;
		// MAY be tracking mouseMove, so unbind this AND cancel polling timer
		$(el).unbind('mousemove');
		_.clearTimer('poll');

		if (s.nextMenuIndex === idx)
			_.clearTimer('open');		// ABORT opening menu for 'this crumb'
		else if (s.openMenuIndex === idx)
			_.timers.close = setTimeout( $.proxy( _, 'hide' ), ms ); // CLOSE menu for 'this crumb'
	}

,	enterMenu: function () {
		this.clearTimer('close');
	}

,	leaveMenu: function () {
		var _	= this;
		_.timers.close = setTimeout( $.proxy( _, 'hide' ), _.options.closeMenuDelay );	// CLOSE menu
	}

,	toggle: function (evt) {
		var _	= this
		,	el	= evt.currentTarget
		if (_.isCurrentMenu( el ))
			_.hide();
		else
			_.show( $(el).parent() );
		evt.stopImmediatePropagation();
		evt.preventDefault();
		return false;
	}

,	show: function ( el_trigger, quickOpen, afterLoad ) {
		var _		= this
		,	s		= _.state
		,	o		= _.options
		,	$link	= $(el_trigger)
		,	idx		= _.getIndex( el_trigger )
		,	reOpen	= (idx === s.openMenuIndex)	// THIS menu is open
		,	position = false // flag for whether menu has been repositioned yet
		;
		if (!_.loadMenus( $link, true )) // true = force loading even if other Ajax running
			return; // menus were not previously loaded, so wait for loadNMenus to call this method when they are ready!

		_.clearTimer('open');

		/* isMouseOver is not always accurate, so menus sometimes will not open!
		if ( o.o.testMouseOver && !$link.isMouseOver( 10, 0 ) ) // allow 10px horizontal variance when 'entering' a crumb
			return; // mouse is NO LONGER over the trigger, so abort opening menu
		*/

		_.clearTimer('close');
		_.clearTimer('test');

		if (reOpen)					// THIS menu is open
			quickOpen = true;		// INSTANT-OPEN this menu
		else if (s.openMenuId) {	// DIFFERENT menu is open
			quickOpen = true;		// INSTANT-OPEN this menu
			_.hide( true );			// HIDE other menu
		}

		var
			$menus	= $(o.menusWrapper).children()
		,	$menu	= $menus.length > idx ? $menus.eq( idx ) : -1
		;
		if ($menu === -1) return; // no menu for this crumb-link (normally only the LAST/current crumb)
		$menu.stop( true, true ); // abort any in-progress animation, just in case

		function setPosition () {
			if (position) return; // already done - don't need to do it twice!
			var tweak	= $.browser.msie ? -1 : 0 // TODO: why do I need a px adjustment for IE?
			,	align	= o.alignMode
			,	offset	= o.offsetLeft +' '+ o.offsetTop
			;
			$link.addClass('menu-open'); // need class assigned before calcing position
			// make menu AT LEAST as wide as trigger-link
			$menu.css('minWidth', Content.cssWidth( $menu, $link.outerWidth() + tweak ))
				.data('initPosition', true); // flag so don't need to show invisibly again

			if (align === 'center') {
				// see if menu will fit within page/container width when 'centered'...
				var $offset_el	= $link.parent().tagName() === 'LI' ? $link.parent() : $link
				,	linkWidth	= Math.floor( $link.outerWidth() )
				, 	menuWidth	= Math.floor( $menu.outerWidth() )
				,	availWidth	= $menu.parent().width()
				,	offsetLeft	= Math.floor( $offset_el.position().left )
				,	offsetRight	= availWidth - offsetLeft - linkWidth
				;
				// if menu CANNOT be perfectly centered, change align to left or right and set an 'offset' for position()
				if (offsetLeft < o.minMenuShift)
					align = 'left';
				else if (offsetRight < o.minMenuShift)
					align = 'right';
				else if ( (menuWidth - linkWidth) / 2 > offsetLeft ) {
					align = 'left';
					offset = ( offsetLeft * -1 ) +' '+ o.offsetTop;
				}
				else if ( (menuWidth - linkWidth) / 2 > offsetRight ) {
					align = 'left';
					offset = ( Math.ceil( availWidth - offsetLeft ) - menuWidth - 1 ) +' '+ o.offsetTop;
				}
			}

			$menu.removeClass('menu-align-left menu-align-center menu-align-right');

			if (align === 'left')
				$menu.position({
					my:			'left top'
				,	at:			'left bottom'
				,	of:			$link
				,	offset:		offset
				,	collision: 'none' // flip, fit, none
				}).addClass('menu-align-left');

			else if (align === 'right')
				$menu.position({
					my:			'right top'
				,	at:			'right bottom'
				,	of:			$link
				,	offset:		offset
				,	collision: 'none' // flip, fit, none
				}).addClass('menu-align-right');

			else if (align === 'center')
				$menu.position({
					my:			'center top'
				,	at:			'center bottom'
				,	of:			$link
				,	offset:		offset
				,	collision: 'none' // flip, fit, none
				}).addClass('menu-align-center');

			position = true;
		};

		// can skip 'invisible positioning' if menu has been opened before
		if ((o.alwaysPosition && !reOpen) || !$menu.data('initPosition'))
			// show menu 'invisibly' so can properly position it
			$.swap( $menu[0], { visibility: "hidden", display: "block" }, setPosition );

		// NOTE: quickOpen must come BEFORE setting vars and classes because .stop() may remove them!
		if (quickOpen || !o.animateOpen) {
			$menu.show();
			setPosition(); // adjust menu position - IF not already done above (see $.swap)
		}
		// if menus just loaded, then double-check that mouse is _still_ over the link
		else // if ( !afterLoad || (o.o.testMouseOver && $link.isMouseOver()) )
			$menu.slideDown( o.animateSpeed, setPosition );
			/*
			$menu.fadeIn(o.animateSpeed, function(){
				if ($.browser.msie && $menu.css("opacity") === 1)
					this.style.removeAttribute('filter'); // OPACITY FIX for IE
			});
			*/
		

		s.openMenuId	= $menu.id();
		s.openMenuIndex	= idx;
		s.nextMenuIndex	= -1;
		$link.addClass('menu-open'); // RE-ADD class in case was removed by .stop()

		// to ensure the menu does not stay open indefinately, add a recurring test to verify mouse is still over it
		if (o.testMouseOver) {
			_.timers.test = setInterval(
				function(){ if ( !$link.add( $menu ).isMouseOver() ) _.hide(); }
			,	Math.max(o.closeMenuDelay, 500)
			);
		}
	}

, 	hide: function (quickClose) {
		var _	= this
		,	o	= _.options
		,	s	= _.state
		,	idx	= s.openMenuIndex // cache value inside closure
		;
		_.clearTimer('close');
		_.clearTimer('test');

		if (idx === -1) return; // no menu is open!
		var $menu = $('#'+ s.openMenuId);

		if ($menu.is(':animated')) {
			$menu.stop( true, true ); // abort in-progress animation, if needed
			quickClose = true;
		}

		if (quickClose === true) { // keep getting phantom integer-values passed as a param!?
			$menu.hide();
			closeDone();
		}
		else
			$menu.stop(true, true).slideUp('fast', closeDone);

		function closeDone () {
			// remove class from trigger-link
			_.getLinkByIndex( idx ).removeClass('menu-open');
			s.openMenuId	= '';
			s.openMenuIndex	= -1;
		}
	}

};


/*
 *	Generic methods for loading & displaying popups
 */
var Popup = {
	open: function ( url, data, options ) {
		var html = '<div class="error">An unexpected error has occurred.</div>' // should never be used!
		,	defaults = {
				title:			''
			,	maxWidth:		false
			,	maxHeight:		false
			,	modal:			true
			,	closeOnEscape:	true
			,	show:			'fade'
			,	buttons: {
					OK:	function (evt, ui) { $(this).dialog('close'); }
				}
			,	close: function (evt, ui) { $(this).remove(); }
			}
		;
		$.ajax({
			data:		data
		,	url:		url
		,	type:		"POST"
		,	xhrFields:	{ withCredentials: true }
		,	timeout:	10000
		,	dataType:	'html' // what is returned

		,	success: function (data, status, xhr) {
				html = data;
			}
		,	error: function (xhr, error) {
				html = '<div class="error">'+ error +'</div>'; // this should never happen!
				this.complete();
			}
		,	complete: function () {
				$('<div style="display:none">'+ html +'</div>')
					.appendTo('body')
					.dialog($.extend( defaults, options ));
			}
		});

		return false; // cancel click or submit
	}

};


/**
 * Cookie Object
 *
 * @description Provides simple syntax to set, retrieve or erase permanent cookies
 */
var Cookie = {
	set:	function (name, value, options) {
		var
			str	= encodeURIComponent(name) +"="+ (value==undefined ? "" : encodeURIComponent(value))
		,	d	= new Date()
		,	o	= options || {}
		,	k, v
		;
		if (typeof o != "object") o = {expires: o};
		if (o.domain === undefined)	o.domain = "root";	// default to 'whole site' instead of 'current path'
		if (o.path === undefined)	o.path = "/";		// default to 'root domain' instead of 'current subdomain'
		else if (o.path === "" || o.path === "current")	delete o.path; // NO path param means 'current path'
		for (k in o) {
			if (k.match(/(expires|max-age|domain|path|secure)/)) {
				v = o[k];
				if (k == "expires") { // value is in 'days'
					if (v == 'never') v = 3650; // 10-year cookie
					else if (v == 'now') v = -1;
					d.setTime(d.getTime() + (24*60*60*1000 * parseFloat(v))); // convert to date
					v = d.toGMTString();
				}
				else if (k == "max-age") // value is in 'days'
					v *= 24*60*60; // convert to 'seconds'
				else if (k == "domain" && v == "root")
					v = Cookie.getRootDomain();
				str += "; "+ k +"="+ v;
			}
		}
		//alert( 'document.cookie = "'+ str +'"' ); // DEBUG
		document.cookie = str;
	}
,	get:	function (name) {
		var cookie = document.cookie.match(new RegExp("(^|;)\\s*" + decodeURIComponent(name) + "=([^;\\s]*)","i"));
		return cookie ? decodeURIComponent(cookie[2]) : null;
	}
,	erase:	function (name, options) {
		var o = options || {};
		if (Cookie.get(name) != null || Cookie.get(name.ucase())) {
			o.expires = -1;
			Cookie.set( name, "", o );
			Cookie.set( name.ucase(), "", o ); // catch uppercase versions too!
			return true
		}
		return false; // cookie did not exist!
	}
,	accept:	function () {
		if (typeof navigator.cookieEnabled=="boolean")
			return navigator.cookieEnabled;
		Cookie.set("_test", "1");
		return (Cookie.erase("_test") = "1");
	}
,	getRootDomain: function () {
		var d = document.location.hostname
		,	i = d.indexOf("."); // 1st dot
		return d.substr(i+1).indexOf(".") > 0 ? d.substr(i) : d; // eg: .webmasterbond.com
	}
};

/**
 * Timer Object
 *
 * @description Simplies re/setting delay-timers
 */
var Timer = {
	timers: {}	
,	set: function ( name, fn, delay ) {
		var T = Timer.timers;
		if (!name) name = fn.name || 't'+ Math.ceil( Math.random() * 9999 );
		if (T[name]) Timer.clear(name);
		T[name] = setTimeout( fn, delay );
		return name;
	}
,	clear: function ( name ) {
		var T = Timer.timers;
		if (!T[name]) return;
		try{ clearTimeout( name ); }catch(ex){};
		T[name] = null;
	}
};


/*
 *	BOUND-EVENT METHODS
 */
function addFocus()		{$(this).addClass("focus");}
function removeFocus()	{$(this).removeClass("focus");}
function addHover()		{$(this).addClass("hover");}
function removeHover()	{$(this).removeClass("hover");}
function addCurrent()	{$(this).addClass("current");}
function removeCurrent(){$(this).removeClass("current");}


/*
 *	GENERIC EVENT HANDLERS
 */
function returnFalse () { return false; }
function changeOnEnter (evt) {
	if (evt.which == $.ui.keyCode.ENTER) {
		evt.stopPropagation();		// prevent Enter from propagating
		$(this).trigger('change');	// trigger change event
		return false;
	}
	return true;
 };
function cancelEnterKey (evt) {
	if (evt.which == $.ui.keyCode.ENTER) {
		evt.stopPropagation();
		return false;
	}
	return true;
 };

function clearValueOnESC (evt) { if (evt.keyCode === 27 && this.value && this.value.length) this.value = ""; };
function clearDefaultValue () { var $E=$(this); if ((this.tagName==='TEXTAREA' || this.type==='text') && this.value === $E.attr('alt')) $E.val(''); if ($E.val()) $E.removeClass('empty'); else $E.addClass('empty'); };
function resetDefaultValue () {
	var $E	= $(this)
	,	val	= this.value
	,	alt	= $E.attr('alt') || '';
	if (this.tagName !== 'TEXTAREA' && this.type !== 'text') return;
	if (val === '' && alt.length)
		$E.val( alt ).addClass('empty');
	else if (val === alt)
		$E.addClass('empty');
	else
		$E.removeClass('empty');
};

function preventFormSubmitOnEnter (evt) {
	if (evt.which != $.ui.keyCode.ENTER || $(evt.target).is("textarea,:button,:submit"))
		return true;
	var focusNext = false;
	$(this).find(":input:visible:not([disabled],[readonly]), a").each(function(){
		if (this === evt.target)
			focusNext = true;
		else if (focusNext) {
			$(this).focus();
			return false;
		}
	});
	return false;
};
 
function preventMousewheelPropagation (evt, delta) {
	// prevent mousewheel from propagating and scrolling the entire page...
	var
		$E		= $(this)
	,	top		= $E.scrollTop()
	,	borders	= $E.cssNum('borderTopWidth') + $E.cssNum('borderBottomWidth')
	,	padding	= $E.cssNum('paddingTop') + $E.cssNum('paddingBottom')
	;
	if (!$E.isMouseOver()) ; // mouse *no longer* over this menu, so abort
	else if (	(delta > 0 && top == 0)
		||	(delta < 0 && $E[0].scrollHeight - top == ($E.outerHeight() - borders))
	) { // menu scrolling has reached either the top or bottom, so abort mousewheel scrolling
		evt.preventDefault();
		//evt.stopPropagation();
		evt.returnValue = false;
	}
};


/*
 *	HELPER METHODS
 */
function fixOpacity (el, ui) {
	if (this && this.tagName) el = this;
	if (typeof el != 'object') return;
	var $E = el.jquery ? el : $(el);
	if ($.browser.msie && $E.css("filter") && $E.css("opacity") == 1)
		$E[0].style.removeAttribute('filter');
};
function fixVisibility (el) {
	if (this && this.tagName)	el = this;
	if (typeof el != 'object')	return;
	$(el).css('visibility','hidden').css('visibility','visible');
};


/*
 *	DOM/LANGUAGE EXTENSIONS
 */

/*
* 	Replacements for typeof
*	is('String', 'test') == true (typeof = 'string')
*	is('String', new String('test')) == true (typeof = 'object')
*	TODO: test $.type to see how it handles string-objects
*/
function objClass (obj) {
	var clas = Object.prototype.toString.call(obj).slice(8, -1);
	return obj !== undefined && obj !== null ? clas : '';
};
function is (type, obj) {
	return type.toLowerCase() === objClass(obj).toLowerCase();
};
function isDefined (obj) { // replace: typeof obj !== 'undefined'
	return typeof obj !== 'undefined';
};


/**
* 	Extend DOM String Element
*/
$.extend( String.prototype, {

	ucase:		function(){return this.toUpperCase();}
,	lcase:		function(){return this.toLowerCase();}

,	trim:		function(){return this.replace(/^\s+|\s+$/g,"");}
	// TODO: trimL & trimR no worky right!
,	trimL:		function(){return this.replace(/\s*((\S+\s*)*)/,"");}
,	trimR:		function(){return this.replace(/((\s*\S+)*)\s*/,"");}

,	empty:		function(){return (this=="");}
,	blank:		function(){return (this.trim()=="");}

,	startsWith:	function (s) {var v=this;return (!v)?false:(s==v.left(s.length));}
,	endsWith:	function (s) {var v=this;return (!v)?false:(s==v.right(s.length));}

,	contains:	function (s,ignoreCase) {
		if (!this.length) return false;
		var as=$.isArray(s)?s:(s.indexOf("|")>0)?s.split("|"):[s];
		for(var i=0,c=as.length;i<c;i++) if((ignoreCase?this.lcase().indexOf(as[i].lcase()):this.indexOf(as[i]))==-1) return false;
		return true;
	}
,	includes:	function (s,ignoreCase) {return this.contains(s,ignoreCase);} // Prototype compatibility
,	between:	function (a,b) {
		var x=this.lcase().charCodeAt();a=a.lcase().charCodeAt();b=b.lcase().charCodeAt();
		return ((x>=a&&x<=b)||(x<=a&&x>=b));
	}

,	mid:		function (a,b) {return this.substr(a,b||9999);}
,	left:		function (c) {return this.substr(0,c);}
,	right:		function (c) {var v=this,l=v.length;if(c>l)c=l;return v.substr(l-c);}
,	fromLeft:	function (c) {var v=this,l=v.length;return c>=l?"":v.substr(c);}
,	fromRight:	function (c) {var v=this,l=v.length;return c>=l?"":v.substr(0,l-c);}
,	before:		function (s) {var v=this,l=v.length,i=v.indexOf(s);return i>=0?v.substr(0,i):v;}
,	after:		function (s) {var v=this,l=v.length,i=v.indexOf(s);return i>=0?v.substr(i+s.length,9999):v;}

,	is:			function (s,ignoreCase) {return (ignoreCase?(this.lcase()==s.lcase()):(this==s));}
,	isAlpha:	function (allowChars) {var v=allowChars,c=v?v:""; return this.length&&!((new RegExp("[^a-z|"+c+"]","i")).test(this));}
,	isNumeric:	function (allowChars) {var v=allowChars,c=v?v:""; return this.length&&!((new RegExp("[^0-9|"+c+"]","i")).test(this));}
,	isAlphanumeric: function (allowChars) {var v=allowChars,c=v?v:""; return this.length&&!(new RegExp("[^a-z|0-9|"+c+"]","i")).test(this.trim());}

,	hasChars:	function(){return (/[a-z]/i).test(this.trim());}
,	hasDigits:	function(){return (/[0-9]/).test(this.trim());}
,	hasSymbols:	function(){return (/[^a-z|0-9]/i).test(this.trim());}
,	hasSpace:	function(){return (/\s/).test(this.trim());}

,	getWords:	function (separatorChars) {
		var s=this,v=separatorChars,c=v?v:"",r=/.+? /g,x,w=[];
		c=",;"+c; // commas and semi-colons are ALWAYS considered word separators
		s=s.replace((new RegExp("["+c+"]","gi"))," ").replace(/\s/g, " ").replace(/ {2,}/g, " ").trim()+" "; // replace ALL separators with a 'single space'
		do{x=r.exec(s);if(x)w.push(x[0].trim());}while(x); // split words at 'spaces'
		return w;
	}
,	countWords:	function (separatorChars) {return this.getWords(separatorChars).length;}
,	countSpaces: function(){var s=this.trim(),r=/ /g,c=0;while(1) if(r.exec(s)) c++; else break; return c;}

});


/**
 * 	Extend DOM Number Object
 *	some methods skipped here /Kevin
 */
$.extend( Number.prototype, {
	between:	function (a,b) {if (a>b){var c=b;b=a;a=c;}return (this>=a&&this<=b );}
,	round:		function (d) {var m=Math.pow(10,d||0);return Math.round(this*m)/m;}
,	floor:		function (d) {var m=Math.pow(10,d||0);return Math.floor(this*m)/m;}
,	ceil:		function (d) {var m=Math.pow(10,d||0);return Math.ceil(this*m)/m;}
,	format:		function (f) {var n=(this+".").split('.'),i=n[0],d=n[1],l=i.length,x,s="";f=(f+".").split(".");
		if(f[0].contains(','))for(x=0;x<l;x++) s+=(x&&(l-x)%3==0?",":"")+i.charAt(x);
		l=f[1].length;if(f[1].contains("0")){while(d.length<l)d+="0";d=d.left(l);}
		return f[0].replace(/[#0,]/g,"")+(s||i)+(d&&l?"."+d:"");}
});



/**
 * 	Extend DOM Array Object
 */
$.extend( Array.prototype, {
	indexOf:	function (val,noCase) {
		var i=this.length;
		while (i--) if (this[i]===val||(noCase&&this[i].ucase()===val.ucase())) return i;
		return -1;
	}
,	contains:	function (val) {return this.indexOf(val)>=0;}
,	removeValue: function (val) {
		var i=this.length;
		while (i--) if (this[i]===val) {this.splice(i,1);break;}
	}
//,	remove:		function (idx) {this.splice(idx,1);}
/**
 *	Array.remove - By John Resig (MIT Licensed)
 *
 *	a.remove(1)		Remove 2nd item
 *	a.remove(-2)	Remove 2nd-to-last item
 *	a.remove(1,2)	Remove 2nd and 3rd items
 *	a.remove(-2,-1)	Remove last and 2nd-to-last items
 */
,	remove: function (from, to) {
		var rest=this.slice((to||from)+1||this.length);
		this.length=from<0?this.length+from:from;
		return this.push.apply(this, rest);
	}
});

if (!Array.prototype.forEach) {
	Array.prototype.forEach = function (fn /*, this_object*/) {
		if (this === void 0 || this === null || typeof fn !== "function") throw new TypeError();
		var i, t = Object(this), c = t.length >>> 0, this_object = arguments[1];
		for (i=0; i<c; i++) if (i in t) fn.call(this_object, t[i], i, t);
	}
	// eg: [].forEach( function (value, index, array) {} )
};


/**
 * 	Extend Date Object
 *	some methods skipped here /Kevin
 */

Date.LANGUAGES="/EN/FR/"; // list of languages to valid 'lang' param passed
Date.MONTH_NAMES_EN=["January","February","March","April","May","June","July","August","September","October","November","December"];
Date.MONTH_ABBREV_EN=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
Date.DAY_NAMES_EN=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
Date.DAY_ABBREV_EN=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
Date.MONTH_NAMES_FR=["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"];
Date.MONTH_ABBREV_FR=["Jan","Fev","Mar","Avr","Mai","Jun","Jul","Aou","Sep","Oct","Nov","Dec"];
Date.DAY_NAMES_FR=["Dimanche","Lundi","Mardi","Mecredi","Jeudi","Vendredi","Samedi"];
Date.DAY_ABBREV_FR=["Dim","Lun","Mar","Mec","Jeu","Ven","Sam"];
Date.getLang = function (lang) {
	if (!lang || Date.LANGUAGES.indexOf("/"+lang.ucase()+"/")==-1) lang="EN"; // default to English
	return lang.ucase();
};

// GENERIC - call with Date.padDigits(vDate) - NOT this.padDigits
Date.padDigits = function (num, digits) {
	if (digits==null) digits=2;
	s=num.toString();
	while (s.length<digits) s="0"+s;
	return s;
	};
// GENERIC - call with Date.from(vDate) - NOT this.from
Date.from = function (vDt, returnTodayIfInvalid) {
	var d,t=typeof vDt;
	if (Date.is(vDt)) return vDt;
	if (t=="number") d=new Date(number);
	//else if (t=="string") d=Date.parse(vDt);
	return d ? d : returnTodayIfInvalid ? Date.now() : null;
	};
// GENERIC - call with Date.now()
Date.now = function (formatString) {
	var n = new Date(), s=formatString;
	return s ? n.format(s) : n;
	};
// check when an object is a Date-object
Date.is = function (v) {return typeof v=="object" && v.getTime;};

$.extend( Date.prototype, {

	// clone the current date object
	clone: function() {
		return new Date(this.getTime());
	}

	// clear the time information from this date and return it
,	clearTime: function() {
		this.setHours(0);
		this.setMinutes(0);
		this.setSeconds(0);
		this.setMilliseconds(0);
		return this;
	}

,	syncTime: function(dt) {
		this.setHours(dt.getHours());
		this.setMinutes(dt.getMinutes());
		this.setSeconds(dt.getSeconds());
		this.setMilliseconds(dt.getMilliseconds());
		return this;
	}

,	getMonthName:	function (lang) {
		return Date["MONTH_NAMES_"+ Date.getLang(lang)][this.getMonth()];
	}

,	getMonthAbbrev:	function (lang) {
		return Date["MONTH_ABBREV_"+ Date.getLang(lang)][this.getMonth()];
	}

,	getDayName:		function (lang) {
		return Date["DAY_NAMES_"+ Date.getLang(lang)][this.getDay()];
	}

,	getDayAbbrev:	function (lang ,len) {
		return Date["DAY_ABBREV_"+ Date.getLang(lang)][this.getDay()];
	}

,	getOrdinalDate:	function () {
		var
			d = this.getDate()
		,	x = d.right(1)
		,	s = ["th","st","nd","rd"]
		;
		return d + s[ x>3 ? 0 : x ];
	}

	// 12-hour clock format
,	get12Hours:		function (options) {
		/* options = {
			addAMPM:		false
		,	lowerCase:		false
		,	padTo2Digits:	false
		}
		*/
		var
			o = options || {}
		,	hr24 = this.getHours()
		,	hr	 = (hr24==0) ? 12 : ((hr24>12) ? hr24-12 : hr24)
		,	k
		;
		if (o.padTo2Digits && hr<10) hr = "0"+ hr;
		if (o.addAMPM) hr += (hr24 < 12) ? "AM" : "PM";
		if (o.lowerCase) hr = hr.toLowerCase();
		return hr;
	}

,	getCSTFormat: function (useDashes, use2Digits) {
		var s=(useDashes) ? "-" : "/", p=Date.padDigits
		, y=this.getFullYear(), m=this.getMonth()+1, d=this.getDate();
		return use2Digits ? p(m)+s+p(d)+s+y : m+s+d+s+y;
	}

	// add format() to Date
,	format:			function (formatString,lang) {
		var out=[], s=formatString,token="",t,c;
		for (var i=0,n=s.length; i<=n; i++) {
			c=i<n?s.charAt(i):"";
			t=token.charAt(0);
			if (c==t)
				token=token.concat(c);
			else {
				out.push(t.match(/[a-z]/i) ? this._convertToken(token) : token);
				token=c;
			}
		}
		return out.join("");
	}

	// internal call to map tokens to the date data
,	_convertToken:	function (s,lang) {
		var _ = this, pad = Date.padDigits,l=lang;
		switch (s.charAt(0)) {
			case "y": // full year
				if (s.length > 2) return _.getFullYear();
				return _.getFullYear().toString().substring (2);
			case "d": // date
				return pad(_.getDate(),s.length);
			case "D": // day in year
				return _.getYearDay();
			case "a": // am/pm
				return _.getHours() > 11 ? "pm" : "am";
			case "A": // AM/PM
				return _.getHours() > 11 ? "PM" : "AM";
			case "H": // hours - 24hrs
				return pad(_.getHours(),s.length);
			case "h": // hours - 24 hrs
				return pad(_.get12Hours(),s.length);
			case "m": // minutes
				return pad(_.getMinutes(),2);
			case "s": // seconds
				return pad(_.getSeconds(),2);
			case "S": // millisecondes
				return pad(_.getMilliseconds(),s.length);
			case "x": // epoch time
				return _.getTime();
			case "Z": // time-zone
				return (_.getTimezoneOffset() / 60) +":"+ pad(_.getTimezoneOffset() % 60,2);
			case "M": // month name - abbrev or long
				if (s.length > 3) return _.getMonthName(l);
				if (s.length > 2) return _.getMonthAbbrev(l);
				return pad(_.getMonth()+1, s.length);
			case "E": // day name - abbrev or long
				if (s.length>3) return _.getDayName(l);
				if (s.length>1) return _.getDayAbbrev(l);
				return _.getDay();
			default:
				return s;
		}
	}

,	getDBFormat: function (useDashes, use2Digits) {
		var s=(useDashes) ? "-" : "/", p=Date.padDigits
		, y=this.getFullYear(), m=this.getMonth()+1, d=this.getDate();
		return use2Digits ? y+s+p(m)+s+p(d) : y+s+m+s+d;
	}

,	getTimeStamp: function (showMilliseconds) {
		var ms=(showMilliseconds) ? ";"+ this.getMilliseconds() : "";
		return this.getHours() +":"+ this.getMinutes() +":"+ this.getSeconds() + ms;
	}

,	isBefore:		function (vDt) {return Date.from(vDt,1) > this;}
,	isAfter:		function (vDt) {return Date.from(vDt,1) < this;}
,	isDateBefore:	function (vDt) {return Date.from(vDt,1).syncTime(this) > this;}
,	isDateAfter:	function (vDt) {return Date.from(vDt,1).syncTime(this) < this;}
});


