/*
*	LinkTix utility classes
*
*	- LTPagination	: Pagination Class with Sorting and Filtering
*	- ltFormManager	: Form Manager Singleton Class (supports required fields and inter-dependency toggling)
*	- ltDialog		: Common Dialog Class
*	- ltAlert		: Replacement for standard alert()
*	- ltConfirm		: Replacement for standard confirm()
*	- ltShowError	: Standard Error Box Display
*	- ltHideError	: Hide the existing error box
*	- ltHelperText	: jQuery Extention to add Helper mouse-over text for links
*	- ltFixDateYear	: In Date fields, fix years that have 2 digits or less
*
*/

// Copyright statement will remain when we compress / minify
var Copyright = '## Copyright (c) 2008-2009 LinkTix, Inc. All rights reserved. ##';

/************************************************************************
 * LTPagination Class
 *
 * Creates a class that can be instantiated multiple times within a page
 ************************************************************************/

// Class Definition
function LTPagination(){};

LTPagination.prototype = {

	// Default Settings
	settings: {
		nPerPage		: 5,		// Number of records per page
		recordContainer	: null,		// DIV container of the records. Required paramter on the first call to init()
		showWhenReady	: null,		// DIV container to unhide when the paginated records are ready to be displayed (after sorting and filtering)
		startSortBy		: null,		// On page load, force a default sorting by this attribute. Ex: "span[class=ticketPrice]"
		startSortLink	: null,		// ALTERNATIVELY, specify the selector for a unique Sorting Link (column sorting) to use for initial sorting
		startSortOrder	: 'asc',	// On page load, force a default sorting in this order. ("asc" or "desc")
		sortLinksClass	: null,		// When using column sorting, this defines the CLASS attribute of these links. ltPagination will add the sort icons next to these links.
		startRecord		: null,		// On page load, automatically go to the page where the specified record is (this is the ID attribute of the record)
		startShowEnd	: 0,		// On page load, automatically go to the last page and highlight the last <x> record (useful when adding records to the end)
		startShowList	: null,		// On page load, automatically find records to highlight from a list array.
									// 	Format: { selector: <selector of elements that have the values to match>, list: <Array of values to search for> }
		stripeColor1	: null,		// 1st color for stripe / zebra effect
		stripeColor2	: null,		// 2nd color for stripe / zebra effect
		next_show_always: true,		// Always show the Next link (even when there is no next page)
		prev_show_always: true,		// Same for Prev
		showRecordCounter: false,	// Show record counters inside <span class="ltpRecordCounter"></span> blocks
		useFirstLast	: false,	// Display First / Last links
		hideSecondaryOnSinglePage : false,	// Hide the Secondary (Top) Pagination Bar when a single page is displayed  (prevents weird UI with 2 pagination bars and a few records)
		pagePrefix		: '',		// Prefix to display before the pagination numbers (ex:  'Page'  =>  Page 1 2 3 4 ... 10)
		pageNumSeparator: '',		// Separator between numbers in the pagination
		navLinksTop		: 'lt-pagination-top',		// Container ID of top navigation links
		navLinksBottom	: 'lt-pagination-bottom',	// Container ID of bottom navigation links
		callBack		: null		// Optional callback function after init() is called
	},

	// Private members
	pArray: null,		// Array of records
	pTop: null,			// top pagination UI
	pBottom: null,		// bottom pagination UI
	pCurrPage: 0,		// Page ID currently displayed
	pLastPage: 1,		// Last Page
	pSelector: null,	// CSS Selector for the records to paginate (filtered)
	pSelectorAll: null,	// CSS Selector for the all records (regardless of filters)
	jpOptions: null,	// jQuery.pagination options
	pHasSortIcons: 0,	// Flag that we have loaded sort icons when the sortLinksClass setting is specified
	pAllRecords: null,	// Cache of the jQuery object that includes all records
	pCurrSorting: null, // Cache the current sorting attributes

	// Pagination Callback
	paginate: function(pgId, container)  {

		// Use the correct instance when paginating
		var pInstance = this;

		// Keep the top/bottom pagination displays in sync (if applicable)
		if (container.get(0).showPage)
		{
			if (container.attr("id") == pInstance.settings.navLinksTop)
			{
				// Timeout is needed to prevent timing conflicts
				var pBottom = $("#"+ pInstance.settings.navLinksBottom);
				setTimeout(function() { 
					pBottom.get(0).showPage(pgId);
					pBottom.contents().each( function() { pInstance.applyUIsettings(pInstance, this) } );
				}, 100);
			}
			else
			{
				// Timeout is needed to prevent timing conflicts
				var pTop = $("#"+ pInstance.settings.navLinksTop);
				if (pTop.length)
					setTimeout(function() { 
						pTop.get(0).showPage(pgId);
						pTop.contents().each( function() { pInstance.applyUIsettings(pInstance, this) } );
					}, 100);
			}
		}

		pInstance.pCurrPage = parseInt(pgId);

		$("span.ltpCurrPage").text( 1+pgId );
		$("span.ltpLastPage").text( (pInstance.pLastPage) ? pInstance.pLastPage : 1 );
		$("span.ltpNumRecords").text( pInstance.pNumRecords );
		$("span.ltpRecordsPlural").text( (pInstance.pNumRecords == 1) ? '' : 's' );

		// Display optional "No Results" box if it exists
		if (pInstance.pNumRecords < 1) {
			$("div#ltpNoResults").fadeIn('fast');
			$("div.ltpResultData").hide();
		}
		else {
			$("div#ltpNoResults").hide();
			pInstance.pAllRecords.hide();
			$("div.ltpResultData").show();

			var from = pgId * pInstance.settings.nPerPage;
			var to = from + pInstance.settings.nPerPage;
			if (to > pInstance.pNumRecords)
				to = pInstance.pNumRecords;

			var stripe1 = pInstance.settings.stripeColor1;
			var stripe2 = pInstance.settings.stripeColor2;
			var showCounter = pInstance.settings.showRecordCounter;
			var row;

			for (var i = from; i < to; i++)
			{
				row = $(pInstance.pArray[i]);
				if (stripe1) {
				 	if (i%2==0)
						row.css({ backgroundColor: stripe1 }).find('.corners_bg').css({ borderColor: stripe1 });
					else
						row.css({ backgroundColor: ((stripe2) ? stripe2 : '#FFF') }).find('.corners_bg').css({ borderColor: ((stripe2) ? stripe2 : '#FFF') });
				}
				if (showCounter)
					row.find('span.ltpRecordCounter').html(i+1);
				row.fadeIn('fast');
			}
		}

		// Apply optional UI settings to the pagination
		if (pInstance.settings.pagePrefix || pInstance.settings.pageNumSeparator || pInstance.settings.useFirstLast) {
			pInstance.pBottom.contents().each( function() { pInstance.applyUIsettings(pInstance, this) } );
			if (pInstance.pTop && pInstance.pTop.length) {
				pInstance.pTop.contents().each( function() { pInstance.applyUIsettings(pInstance, this) } );
			}
		}
	},

	applyUIsettings: function(pInstance, el) {
		var link = $(el);
				if (pInstance.settings.useFirstLast) {
					if (link.hasClass('prev'))
						link.next().clone(true).html('First').addClass('first prev').insertBefore(link);
					else if (link.hasClass('next'))
						link.prev().clone(true).html('Last').addClass('last next').insertBefore(link);

			// Fix IE7 issue with Next/Last Positioning
			setTimeout(function() {
				if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) == 7) {
					$('.pagination .next, .pagination .last').css({ marginTop: '-17px' });
				}
			}, 100);
				}

				if (pInstance.settings.pagePrefix && link.text()=='1')
					link.before('<span>'+ pInstance.settings.pagePrefix +'</span>');

		if (pInstance.settings.pageNumSeparator && !isNaN(link.text()) && link.next().text() && !isNaN(link.next().text()))
			link.after( '<span>'+ pInstance.settings.pageNumSeparator +'</span>' );
	},

	// Returns the background color of an element
	getBackgroundColor: function (e) {
		try {
		  var c = jQuery.css(e, "background-color");
		  if ( c.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i) && e.parentNode )
		     return backgroundColor(e.parentNode);
		  if (c==null)
		    return "#ffffff";
		  if (c.indexOf("rgb") > -1)
			  c = rgb2hex(c);
		  if (c.length == 4)
		    c = hexShort2hex(c);
		  return c;
		} catch(err) {
		  return "#ffffff";
		}
	},

	// Sort records from a link with sort icons
	// DEPENDENCY: To use this, the ltPagination.settings.sortLinksClass setting must be defined so that the sort Icons are added after a sorting link/column is clicked
	sortLink: function(caller, attr, asNum) {
		// We need the caller link object to continue
		if (!caller) return true;

		// We need sort icons to continue
		var icons = $('img.sortIcon');
		if (!this.pHasSortIcons || !icons.length) return true;

		var link = $(caller);

		// Hide all sort icons (we use Opacity to maintain screen real-estate
		icons.css({opacity: 0});

		// Determine sort direction based on existing sort and reversing if needed
		var sortDirection = "asc";
		if (link.hasClass("sort_asc")) {
			sortDirection = "desc";
			link.removeClass("sort_asc");
		}
		else {
			// Remove any existing sorting
			$('a.sort_asc,a.sort_desc').removeClass("sort_asc").removeClass("sort_desc");
		}

		// Add the class for this direction
		link.addClass("sort_"+ sortDirection);

		// Show the up/down icon
		var sortIcon = link.next();
		if (sortIcon.hasClass("sortIcon")) {
			var src = sortIcon.attr("src");
			sortIcon.attr("src",src.replace(/sort_[a-z]+\.gif$/,'sort_'+ sortDirection +'.gif')).css({opacity: 1});
		}

		// Apply the sorting
		// If "attr" is not passed, we simply "show the existing sorting" since the initial sort is done on the server-side
		if (attr)
			this.sortRecords(attr, sortDirection, asNum, true);
	},

	/*
	** Sort records from a drop-downs with explicit sorting
	** Format of option value: "X:Y:Z"
	**		Where X is either "A" for Ascending or "D" for Descending
	**			  Y is the CSS Selector to get the value of each record to use for sorting
	**			  Z is "N" to sort numerically or "A" to sort alphabetically
	** Example Format of option value:  "A:span[class=eventTitleFilter]:A"
	**		This means that we will sort "A"scending, using the innerHTML of a span with class "eventTitleFilter", sorting as alphabetically
	*/
	sortSelect: function(caller) {
		sortParams = $(caller).val().split(/\:/g);

		if (sortParams && sortParams.length==3)
			this.sortRecords(sortParams[1], (sortParams[0]=='D') ? 'desc' : 'asc', (sortParams[2]=='N'));
	},

	// Performs the sort by calling tinySort on the records and repaginate
	sortRecords: function(attr, sortDirection, asNum, sortByAttribute, afterDialog) {
		var allRecs=this.pAllRecords;
		var dialog = $("div#ltpSortDiv");
		var pInstance=this;

		// If we have a "Sorting..." dialog, show it when appropriate
		if (!afterDialog && dialog.length && allRecs.length > 500) {
			// Show that we are loading results by unhiding a dialog
			dialog.get(0).style.display='block';

			// Give time to dialog to appear, then sort
			setTimeout(function(){ pInstance.sortRecords(attr, sortDirection, asNum, sortByAttribute, true) }, 10);
			return;
		}

		// Check if we can reverse instead of sorting
		var doReverse = (this.pCurrSorting == attr);
		this.pCurrSorting = attr;

		// Perform the Sort or a Reverse (which is quicker)
		var recArray = allRecs.get();
		if (doReverse) {
			recArray.reverse();
		}
		// Sorting by attribute is a lot faster
		else if (sortByAttribute) {
			recArray.sort(sortDirection == 'desc'
				? function( a, b ) {
					var x = $(a).attr(attr);
					var y = $(b).attr(attr);
					if (asNum) {
						x = parseFloat(x);
						y = parseFloat(y);
					}
					else {
						x = x ? x.toLowerCase() : '';
						y = y ? y.toLowerCase() : '';
					}
					return (x<y?1:(x>y?-1:0));
				}
				: function( a, b ) {
					var x = $(a).attr(attr);
					var y = $(b).attr(attr);
					if (asNum) {
						x = parseFloat(x);
						y = parseFloat(y);
					}
					else {
						x = x ? x.toLowerCase() : '';
						y = y ? y.toLowerCase() : '';
					}
					return (x<y?-1:(x>y?1:0));
				});
		}
		// Kept for backward compatibility
		// TODO: Move all sorting pages to use Attribute Sorting
		else {
			recArray.sort(sortDirection == 'desc'
				? function( a, b ) {
					var x = $(a).find(attr)[0].innerHTML;
					var y = $(b).find(attr)[0].innerHTML;
					if (asNum) {
						x = parseFloat(x);
						y = parseFloat(y);
					}
					else {
						x = x ? x.toLowerCase() : '';
						y = y ? y.toLowerCase() : '';
					}
					return (x<y?1:(x>y?-1:0));
				}
				: function( a, b ) {
					var x = $(a).find(attr)[0].innerHTML;
					var y = $(b).find(attr)[0].innerHTML;
					if (asNum) {
						x = parseFloat(x);
						y = parseFloat(y);
					}
					else {
						x = x ? x.toLowerCase() : '';
						y = y ? y.toLowerCase() : '';
					}
					return (x<y?-1:(x>y?1:0));
				});
		}

		// Display the sorted records by reapplying the DOM records to the container
		var container = this.pAllRecords.eq(0).parent();
		container.append.apply( container, recArray );

		// Close the dialog
		if (afterDialog) {
			dialog.get(0).style.display='none';
		}

		// Trigger a custom event for any listeners that may want to know when the sorting is complete
		$('body').trigger('LT_SortingComplete');

		setTimeout(function() { pInstance.init(null,0,1); }, 10); // Init + Reset to 1st page
	},

	// * Not Used for now *
	// This is used to do an append on a large number of records, by chunking the processing and preventing "Script Blocking"
	longAppend: function(container, array, callBack, laCurrent) {
		var pInstance = this;
		if (!laCurrent) laCurrent=0;
		var laTo = laCurrent+30;
		var aLength = array.length;
		for(i=laCurrent; i < laTo; i++) {
			if (i >= aLength) {
				this.isSorting=false;
				if (callBack) callBack();
				return;
			}
			container.append( array[i] );
		}
		setTimeout(function() { pInstance.longAppend(container, array, callBack, laTo) }, 50);
	},

	// Performs a filter based on a criteria function and resets to the first page
	filterRecords: function(criteria, afterDialog) {
		var allRecs=this.pAllRecords;
		if (!allRecs) return;

		var dialog = $("div#ltpFilderDiv");
		var pInstance=this;

		// If we have a "Filtering..." dialog, show it when appropriate
		if (!afterDialog && dialog.length && allRecs.length > 500) {
			// Show that we are loading results by unhiding a dialog
			dialog.get(0).style.display='block';

			// Give time to dialog to appear, then sort
			setTimeout(function(){ pInstance.filterRecords(criteria, true) }, 10);
			return;
		}

		if (criteria==null) {
			allRecs.hide().removeClass('ltpFiltered');	// Unfilter
		}
		else {
			allRecs
				.hide()
				.addClass('ltpFiltered')		// Filter All
				.filter(criteria)			// Unfilter per criteria function
				.removeClass('ltpFiltered');
		}

		setTimeout(function() { pInstance.init(null,0,1); if (afterDialog) { dialog.get(0).style.display='none' } }, 10); // Init + Reset to 1st page
	},

	// Restore the pagination after a record is inserted / removed. Tries to stay on the current page
	restore: function() {
		this.init(null,1);	  // Init + Restore while preserving current page
	},

	// Unhide the main container to prevent "page flashing"
	showPageContainer: function(specifiedContainer) {
		var container = (specifiedContainer) ? specifiedContainer : this.settings.showWhenReady;
		if (container) {
			$('#'+ container).css({ display: 'block' }).show();
			this.settings.showWhenReady = null; // Prevent multiple calls
		}
	},

	// Go to the page where a specific record is displayed
	navToRecord: function(recordId) {
		var pInstance = this;
		for (i=0; i < pInstance.pNumRecords; i++) {
			if (pInstance.pArray[i].id == recordId) {
				var pgToNav = Math.floor(i / pInstance.settings.nPerPage)
				setTimeout(function() {
					pInstance.paginate(pgToNav, pInstance.pBottom);
					setTimeout(function() {
						if (pInstance.pTop && pInstance.pTop.length)
							pInstance.pTop.get(0).showPage(pgToNav);

						if(pInstance.pBottom && pInstance.pBottom.get(0).showPage)
							pInstance.pBottom.get(0).showPage(pgToNav);
					}, 200);
 				}, 200);
				return;
			}
		}
	},

	// Go to the last page. Useful when adding records to the end of a list
	navToEnd: function() {
		var pInstance = this;
		var pgToNav = Math.floor((pInstance.pNumRecords-1) / pInstance.settings.nPerPage);
		pInstance.paginate(pgToNav, pInstance.pBottom);
		return pgToNav;
	},

	// Highlight records that are added/updated
	processHighlighting: function(pInstance) {
		// Check for highlight the end records when just added
		if (pInstance.settings.startShowEnd) {
			var hiEnd = pInstance.settings.startShowEnd;
			for (var hidx=1; hidx <= hiEnd; hidx++)
				ltHighlightFade($(pInstance.pArray[ pInstance.pNumRecords - hidx ]), 2000);
		}

		// Check for highlight specific records after finding them
		var startShowList = pInstance.settings.startShowList;
		if (startShowList) {
			// Create a list to search, trimming any whitespace between separators
			var listString = ','+ startShowList.list.join(',').toLowerCase().replace(/,[\s\t]+/g,',').replace(/[\s\t]+,/g,',') +',';
			$(pInstance.pSelector).each(function() {
				var record = $(this);
				var valueToFind = jQuery.trim(record.find(startShowList.selector).text().toLowerCase());
				if (listString.indexOf(','+ valueToFind +',') > -1)
					// Only highlight records that are in view
					if (record.is(':visible'))
						ltHighlightFade(record, 2000);
			});
		}
	},

	// Init Method called every time a new set of records are loaded
	init: function(options, doRestore, doReset) {

		// Apply any options
        if(options) {
            $.extend(this.settings, options);
        }

		// Without an existing recordContainer, we cannot continue
		if (!this.settings.recordContainer)
			return;

		// If Column Sorting is enabled, we need to add the sort icons
		var sortLinksClass = this.settings.sortLinksClass;
		if (sortLinksClass && !this.pHasSortIcons) {
			// Flag to prevent adding icons more than once
			this.pHasSortIcons = true;

			// Preload the descending image
			window.preloaded_descImg = (new Image()).src="/linktix/static/images/sort_desc.gif";
			$("a."+ sortLinksClass)
				// Append the up icons to all sort links
				.after('<img class="sortIcon" src="/linktix/static/images/sort_asc.gif" />')
				// now ensure they don't wrap
				.parent().css({ whiteSpace: 'nowrap' })
				// now make the icons invisible (using opacity to reserve screen real-estate and prevent column shifting when sorting)
				.find('img.sortIcon').css({ opacity: 0 });
		}

		// Sets the selector for the records to paginate
		this.pSelector = 'div#'+ this.settings.recordContainer +' > div:not(div.evspacer,div#ltpNoResults,div.ltpFiltered)'; // Skips special DIVs and Filtered DIVs
		this.pSelectorAll = 'div#'+ this.settings.recordContainer +' > div:not(div.evspacer,div#ltpNoResults)'; // Skips special DIVs only

		// Array of event objects for sorting / pagination
		this.pArray = $(this.pSelector).hide().get();
		this.pNumRecords = this.pArray.length;

		// Cache the total records for speed
		this.pAllRecords = $(this.pSelectorAll);

		if (!doRestore)
			this.pCurrPage = 0;

		this.pLastPage = Math.ceil(this.pNumRecords / this.settings.nPerPage);

		$("span.ltpCurrPage").html( 1 );
		$("span.ltpLastPage").text( this.pLastPage );

		this.pBottom = $("#"+ this.settings.navLinksBottom);
		if (!this.pBottom.length) {
			this.showPageContainer();
			if (this.settings.callBack) {
				this.settings.callBack();
				this.settings.callBack = null; // Prevent multiple calls
			}
			return;
		}

		// For less than 2 pages, no need for pagination
		// NOTE: For now, we always show the pagination, so we use lastPage < 1 to ensure it's always displayed
		if (this.pLastPage < 1) {
			this.paginate(0, this.pBottom);
			$("div.pagination").hide();

			// Apply optional initial sorting
			if (!doReset && !doRestore) {
				if (this.settings.startSortBy) {
					this.sortRecords(this.settings.startSortBy, this.settings.startSortOrder);
				}
				else if (this.settings.startSortLink) {
					if (this.settings.startSortOrder=='desc')
						$(this.settings.startSortLink).addClass("sort_asc").trigger('click') ;
					else
						$(this.settings.startSortLink).trigger('click');
					return;
				}

				var pInstance = this;
				setTimeout(function() {
					pInstance.showPageContainer();
					pInstance.processHighlighting(pInstance);

					if (pInstance.settings.callBack) {
						pInstance.settings.callBack();
						pInstance.settings.callBack = null; // Prevent multiple calls
					}
				}, 500);
			}
			return;
		}
		$("div.pagination").show();

		var currInstance = this;

		// jQuery.pagination options
		this.jpOptions = {
			num_display_entries: 3,
			num_edge_entries: 1,
			items_per_page: this.settings.nPerPage,
			prev_text:"Previous",
			next_text:"Next",
			next_show_always: this.settings.next_show_always,
			prev_show_always: this.settings.prev_show_always,
			link_to: "javascript://",
            callback: function(a,b) { currInstance.paginate(a,b) }
		};

		// Prepare top pagination if used
		var pInstance = this;
		this.pTop = $("#"+ this.settings.navLinksTop);
		if (this.pTop && this.pTop.length) {
			this.pTop.pagination(this.pNumRecords, this.jpOptions);

			// Optionally hide the secondary pagination bar
			if (this.settings.hideSecondaryOnSinglePage && this.pLastPage < 2) {
				setTimeout(function() { $("div.ltpPaginationSecondary").hide();pInstance.pTop.hide(); }, 10);
			}
		}

		this.pBottom.pagination(this.pNumRecords, this.jpOptions);

		// Apply optional initial sorting
		if (!doReset && !doRestore) {
			if (this.settings.startSortBy) {
				this.sortRecords(this.settings.startSortBy, this.settings.startSortOrder);
			}
			else if (this.settings.startSortLink) {
				if (this.settings.startSortOrder=='desc')
					$(this.settings.startSortLink).addClass("sort_asc").trigger('click');
				else
					$(this.settings.startSortLink).trigger('click');
				return;
			}
		}

		// If requested, go to the page where a specified record is
		var startRecord = pInstance.settings.startRecord;
		if (startRecord) {
			pInstance.navToRecord( startRecord );
			pInstance.settings.startRecord = null;
		}
		// If requested, go to the page to highlight page and highlight the records from a list
		else if (pInstance.settings.startShowList && !doReset && !doRestore) {
			// Scan records until we find the first one in the list, then navigate to this record
			var startShowList = pInstance.settings.startShowList;
			var listString = ','+ startShowList.list.join(',').toLowerCase().replace(/,[\s\t]+/g,',').replace(/[\s\t]+,/g,',') +',';
			var found=false;
			for (i=0; i < pInstance.pNumRecords; i++) {
				var valueToFind = jQuery.trim($(pInstance.pArray[ i ]).find(startShowList.selector).text().toLowerCase());
				if (listString.indexOf(','+ valueToFind +',') > -1) {
					var pgToNav = Math.floor(i / pInstance.settings.nPerPage)
					setTimeout(function() {
						pInstance.paginate(pgToNav, pInstance.pBottom);
						setTimeout(function() {
							if (pInstance.pTop && pInstance.pTop.length)
								pInstance.pTop.get(0).showPage(pgToNav);
							pInstance.pBottom.get(0).showPage(pgToNav);

							// Then do the highlighting for all these records
							pInstance.processHighlighting(pInstance);
						}, 200);
	 				}, 200);
	 				found=true;
					break;
				}
			}
			// Default to the 1st page if we can't find the record
			if (!found) {
				pInstance.paginate(0, pInstance.pBottom);
			}
		}
		// If requested, go to the last page and highlight the last record
		else if (pInstance.settings.startShowEnd && !doReset && !doRestore) {
			var pgToNav = pInstance.navToEnd();
			setTimeout(function() {
				if (pInstance.pTop && pInstance.pTop.length)
					pInstance.pTop.get(0).showPage(pgToNav);
				pInstance.pBottom.get(0).showPage(pgToNav);

				pInstance.processHighlighting(pInstance);
			}, 200);
		}
		// Try to preserve the page if requested and if possible
		else if (doRestore && pInstance.pCurrPage < pInstance.pLastPage) {
			pInstance.paginate(pInstance.pCurrPage, pInstance.pBottom);
			setTimeout(function() {
				if (pInstance.pTop && pInstance.pTop.length)
					pInstance.pTop.get(0).showPage(pInstance.pCurrPage);
				pInstance.pBottom.get(0).showPage(pInstance.pCurrPage);
			}, 200);
		}
		// Just go to the 1st page
		else
			pInstance.paginate(0, pInstance.pBottom);

		// Unhide the page container
		pInstance.showPageContainer();

		if (pInstance.settings.callBack) {
			pInstance.settings.callBack();
			pInstance.settings.callBack = null; // Prevent multiple calls
		}
	}
};

// Base instance created for backward compatibility
// Other instances should be created as needed for multiple pagination controls on the same page.
var ltPagination = new LTPagination();


/**********************
 * Form Manager Class
 **********************/
var ltFormManager = {

	// Default Settings
	settings: {
		formId		: null,		// ID of the Form to manage
		toggleFields: null,		// Optional Array of objects to trigger field disabling
								//  Format: [{	triggerFields	: '<fields selector>',
								//				triggerValue	: '<value that triggers disabling>',
								//				fieldsToDisable	: '<fields selector>'
								//			 },
								//			 ... ]
		validations : null,		// Optional Array of objects to define client-side validations of form fields
								//  Format: [{	formFields		: '<fields selector>',
								//				// asNumeric is optional
								//				asNumeric		: { integerLength: 10,  // Default is no limit
								//									decimalLength: 2,   // Default is no limit
								//									allowNegative: true }, // Default is true
								//				// asText is optional
								//				asText			: { validExpression: /^[a-z]+$/ }, // Required regular expression. Field is valid if the Expression is matched
								//			 },
								//			 ... ]
		showRequired: false,	// Display (*) indicators next to required fields
		showSaving: "Saving...",// Message to display when submitting the form. If blank, we don't show a message
		beforeSubmitCheck: null,// Optional function to call before submitting. If the function returns false, the submit is cancelled.
								//  Useful for custom validations like conditional required fields
		callBack	: null		// Optional callback function after init() is called
	},

	// Private members
	managedForm 	: null,		// Form to Manager
	numOmitted		: null,		// Number of fields that the user didn't fill
	firstOmitted	: null,		// First Field that was omitted

	// Returns the value of a field element (extrends jQuery for checkboxes)
	getFieldValue: function(field) {
		if (field.get(0).type == 'checkbox')
			return ''+ field.get(0).checked;

		return field.val();
	},

	// Toggle specified field(s) (enable/disable) when the caller is clicked on (radio button or checkbox)
	toggleField: function(caller, fieldSelector, triggerValue) {
		var field = $(fieldSelector);
		var callerObj = caller.get(0);

		var doDisable = (triggerValue == ltFormManager.getFieldValue(caller));

		field
			// Toggle disabling of the fields
			.attr('disabled', doDisable)
			.css({ backgroundColor: (doDisable) ? '#DDD' : '#FFF' });

		// If any of the fields are date picker fields
		if (field.datepicker)
			field.datepicker((doDisable) ? 'disable' : 'enable');
	},

	// Perform Validations on a specific field
	validateField: function(vField, fieldSettings) {
		var asNumeric = fieldSettings.asNumeric;
		var asText = fieldSettings.asText;
		var val = vField.val();

		// Check for numeric validations
		if (asNumeric) {
			val = val.replace(/[\s\t\,]/g, '');  // Remove spaces and commas
			if (val) {
				vField.val( val );
				var validChars = (asNumeric.allowNegative) ? /^[0-9\.-]+$/ : /^[0-9\.]+$/;
				if (val.search(validChars) < 0) {
					return (vField.attr('fieldLabel') || vField.attr('name'));  // In case we don't find a label, we default to the field name
				}
				val = parseFloat(val);

				var decomp = (''+val).match(/^([0-9]+)(\.?)([0-9]*)$/);
				if (decomp) {
					if (decomp[1].length > asNumeric.integerLength) {
						// Integer too long
						return vField.attr('fieldLabel');
					}

					// Ensure we keep the proper format based on the # of decimals
					if (typeof(asNumeric.decimalLength) == 'number') {
						if (decomp.length > 3 && decomp[2] && decomp[3]) {
							// Check if the field has too many decimals
							if (decomp[3].length > asNumeric.decimalLength)
								val = decomp[1] +'.'+ decomp[3].substr(0,asNumeric.decimalLength);
							// Too fiew decimals?
							else if (decomp[3].length < asNumeric.decimalLength) {
								var zeros = '';
								for (var i=0; i < asNumeric.decimalLength-decomp[3].length; i++)
									zeros += '0';

								val += zeros;
							}
						}
						else { // No decimals?
							var zeros = (''+Math.pow(10,asNumeric.decimalLength)).substr(1);
							val = parseInt(val) +'.'+ zeros;
						}

						vField.val( (''+val).replace(/\.$/,'') );
					}
				}
			}
		}
		// Check for text validations using regular expressions
		else if (asText && asText.validExpression && val) {
			var validChars = asText.validExpression;
			if (val.search(validChars) < 0) {
				return (vField.attr('fieldLabel') || vField.attr('name'));  // In case we don't find a label, we default to the field name
			}
		}

		return "";
	},

	// Process Form Submit
	processForm: function() {
		ltHideError();

		// Enforce required fields
		ltFormManager.numOmitted = 0;
		ltFormManager.managedForm.find("input").css({ backgroundColor: '#FFF' });
		var reqFields = ltFormManager.managedForm.find('input.fmIsRequired, select.fmIsRequired, textarea.fmIsRequired').css({ backgroundColor: '#FFF' });
		var fieldsOmitted=null;

		reqFields.each(function() {
			var field = $(this);
			if (field.val().search(/[^\s\t]+/) < 0) {
				if (!fieldsOmitted)
					fieldsOmitted = [];

				fieldsOmitted[ fieldsOmitted.length ] = field.attr('fieldLabel');

				ltFormManager.numOmitted++;
				if (ltFormManager.numOmitted==1)
					ltFormManager.firstOmitted = field;
				field.css({ backgroundColor: '#FFB5B5' });
			}
		});

		if (ltFormManager.numOmitted) {
			// Construct the message based on the error
			var msg, title = "Missing Required Field";
			if (ltFormManager.numOmitted == 1)
				msg = 'Please enter a'+ ((fieldsOmitted[0].charAt(0).match(/^[aeiou]/i)) ? 'n ' : ' ') + fieldsOmitted[0];
			else
			{
				msg = 'The following fields are required:<ul>';
				var msgRow;
				for (var i=0; i < ltFormManager.numOmitted; i++) {
					msgRow = '<li>'+ fieldsOmitted[i] +'</li>';

					// Only show each field once
					if (msg.search(msgRow) < 0)
						msg += msgRow;
				}

				msg += '</ul>';
				title += 's';
			}

			ltShowError(msg, title);
			parent.$("#sQuery").focus().blur(); // Scroll to top

			delete fieldsOmitted;
			return false;
		}

		// Enforce Validations
		var fieldsToValidate = ltFormManager.settings.validations;
		var fieldsNotValid=null;
		if (fieldsToValidate) {
			var numInvalid = 0;
			for (var i=0; i < fieldsToValidate.length; i++) {
				$(fieldsToValidate[i].formFields).each( function() {
					var field = $(this);
					var invalidField = ltFormManager.validateField( field, fieldsToValidate[i] );

					if (invalidField) {
						if (!fieldsNotValid)
							fieldsNotValid = [];

						fieldsNotValid[ fieldsNotValid.length ] = invalidField;

						numInvalid++;
						field.css({ backgroundColor: '#FFB5B5' });
					}
				});
			}
			if (numInvalid) {
				// Construct the message based on the error
				var msg, title = "Invalid Format";
				if (numInvalid == 1)
					msg = 'The '+ fieldsNotValid[0] +' is not in a valid format';
				else
				{
					msg = 'The following fields are not in a valid format:<ul>';
					var msgRow;
					for (var i=0; i < numInvalid; i++) {
						msgRow = '<li>'+ fieldsNotValid[i] +'</li>';

						// Only show each field once
						if (msg.search(msgRow) < 0)
							msg += msgRow;
					}

					msg += '</ul>';
				}

				ltShowError(msg, title);
				parent.$("#sQuery").focus().blur(); // Scroll to top

				delete fieldsNotValid;
				return false;
			}
		}

		var customCheck = ltFormManager.settings.beforeSubmitCheck;
		if (customCheck && !customCheck())
			return false;

		if (ltFormManager.settings.showSaving && parent.showLoading)
			parent.showLoading(ltFormManager.settings.showSaving);

		return true;
	},

	// Check for server-side errors and displays an error message if applicable
	// Returns true if an error was found, false otherwise
	processSSErrors: function() {
		var errorMsg = '', ssErrors = [];
		$('span.ssErrors').each(function() {
			var field = $(this);
			var error = $.trim(field.html());
			if (error) {
				var relatedField = field.attr('relatedField');

				// If the error is an exception, let's show a generic message and add the full message in "View Source" comments
				if (error.search(/exception is/i) > 0)
					error = 'Invalid Format '+ (relatedField ? ('for '+ relatedField) : '') +'<span style="display:none">'+ error +'</span>';

				ssErrors.push(error);

				// Highlight the related field, if supplied
				if (relatedField)
					$('#'+ relatedField).css({ backgroundColor: '#FFB5B5' });
			}
		});

		var numErrors = ssErrors.length;
		if (numErrors == 0) {
			return false;
		}
		else if (numErrors > 1) {
			errorMsg = '<ul style="margin-left:0; padding-left:20px">';
			for(i=0; i < numErrors; i++) {
				errorMsg += '<li>'+ ssErrors[i] +'</li>';
			}
			errorMsg += '</ul>';
		}
		else
			errorMsg = ssErrors[0];

		ltShowError(errorMsg, "Validation Error"+ ((numErrors > 1) ? 's' : ''));
		parent.$("#sQuery").focus().blur(); // Scroll to top

		return true;
	},

	// Form Validation Disabler
	// This allows testing of server-side validation by disabling restrictions on fields, client-side validations and allowing editing of HIDDEN and SELECT fields
	breakFree: function() {
		// Remove Maxlength from INPUT and TEXTAREA fields and highlight them
		// Also remove events that could process validations, and enables disabled fields
		$('input:text,input:password,textarea')
			.attr('maxlength','99999999')
			.css({ backgroundColor: '#FF0' })
			.removeAttr('disabled').removeAttr('readonly').removeAttr('onchange')
			.unbind('change').unbind('keypress').unbind('keydown').unbind('keyup').get(0).onchange=null;

		// Turn HIDDEN and SELECT fields into INPUT fields
		$('input[type=hidden],select').each(function() {
			var field = $(this);
			field.after('<span>HIDDEN_'+ field.attr('name') +':<input type="text" title="Hidden Field: '+ field.attr('name') +'" id="'+ field.attr('id') +'" name="'+ field.attr('name') +'" value="'+ field.val() +'" style="background-color: #FF0" /></span>');
			field.remove();
		});

		// Disable Form onSubmit validation catching
		if (this.managedForm && this.managedForm.length)
			this.managedForm.get(0).onsubmit=null;
	},

	// Init Method
	init: function(options) {
		// Apply any options
        if(options) {
            $.extend(ltFormManager.settings, options);
        }

		// Without an existing Form to manage, we cannot continue
		ltFormManager.managedForm = $('form#'+ ltFormManager.settings.formId);
		if (!ltFormManager.managedForm.length)
			return;

		// Prepare required fields
		ltFormManager.managedForm.find('input.fmIsRequired, select.fmIsRequired, textarea.fmIsRequired').each(function() {
			var reqField = $(this);

			// Store Label if not specified
			if (!reqField.attr('fieldLabel')) {
				// See if the field was tagged with the ID of the label
				var lblId = reqField.attr('class').match(/fmLbl_([^\s\t]+)/);
				if (lblId)
					reqField.attr('fieldLabel', $('#'+ lblId[1]).text());
				else
					// Otherwise, look for the previous tag's label (Hack)
					reqField.attr('fieldLabel', reqField.parent().prev().text().replace(/\:$/,''));
			}

			// Add Required Field indicator
			if (ltFormManager.settings.showRequired)
				reqField.parent().append('&nbsp;<span style="color: #E5924A; font-weight: bold;">(*)</span>');  //TODO: Use Class
		});

		// Prepare input fields
		ltFormManager.managedForm.find('input').each(function() {
			var inputField = $(this);
			// Store Label if not specified
			if (!inputField.attr('fieldLabel')) {
				// First see if the field was tagged with the ID of the label
				var lblId = inputField.attr('class').match(/fmLbl_([^\s\t]+)/);
				if (lblId)
					inputField.attr('fieldLabel', $('#'+ lblId[1]).text());
				else
					// Otherwise, look for the previous tag's label (Hack)
					inputField.attr('fieldLabel', inputField.parent().prev().text().replace(/\:$/,''));
			}
		});

		// Perform Validations before Submit
		ltFormManager.managedForm.get(0).onsubmit = ltFormManager.processForm;

		// Prepare toggle fields (fields that trigger the disabling of other fields)
		var togFields = ltFormManager.settings.toggleFields;
		if (togFields) {
			for (var i=0; i < togFields.length; i++) {
				$(togFields[i].triggerFields).each(function() {
					var thisTrigger = $(this);
					var fToDisable = togFields[i].fieldsToDisable;
					var trigValue  = togFields[i].triggerValue;

					// Bind the click event of the triggers
					thisTrigger.bind('click', function() { ltFormManager.toggleField(thisTrigger, fToDisable, trigValue) });

					// Simulate initial click to potentially disable dependent fields
					var actualField = thisTrigger.get(0);
					if ((actualField.type != 'radio' || actualField.checked) && trigValue == ltFormManager.getFieldValue(thisTrigger)) {
						thisTrigger.triggerHandler('click');
					}
				});
			}
		}

		// Check for server-side errors
		ltFormManager.processSSErrors();

		if (ltFormManager.settings.callBack)
			ltFormManager.settings.callBack();
	}
};


/************************************************************************
 * Dialog Class - Depends on facebox.js
 *
 * Creates a class that can be instantiated multiple times within a page
 ************************************************************************/

// Class Definition
function LTDialog(options) {
	// Constructor
	this.init(options);
};

LTDialog.prototype = {

	/* Public Methods */

	// Show the dialog
	showDialog: function() {
		var content = this.settings.html;
		if (content) {
			if (this.settings.width)
				$.facebox.settings.boxWidth = this.settings.width;

			launchFacebox(content);
			if (typeof(content) == 'string' && content.search(/dynamicBtn/) > 0)
				$('a.dynamicBtn').corners("transparent");
		}
		return this;
	},

	// Hide the dialog
	hideDialog: function() {
		$(document).trigger('close.facebox');
		return this;
	},

	// Set the Dialog Title
	setTitle: function(newTitle) {
		this.settings.title = newTitle;
		return this;
	},

	// Set the Dialog HTML
	setHTML: function(newHTML) {
		this.settings.html = newHTML;
		return this;
	},

	// Set the Dialog content as a URL
	setURL: function(url, doScroll) {
		// We construct a DIV container that includes a spinner and an iFrame which loads the URL
		// Since the URL will use ltUtils.js, that page will automatically hide the spinner (ltdSp) when it is loaded
		var divc = '<div';

		// Add a flag to the URL so that the spinner will be removed when the iFrame has loaded
		var finalURL = url + ((url.search(/\?/) > 0) ? '&' : '?') +'indl=1';

		if (this.settings.width)
			divc += ' style="width:'+ this.settings.width +'px; height:'+ this.settings.height +'px"';

		divc += '><div id="ltdSp" style="padding-top:30px;text-align:center"><img src="/linktix/static/images/spinner.gif" /><\/div>'
				+'<iframe scrolling="'+ ((doScroll) ? 'auto' : 'no' )
				+'" frameborder="0" marginheight="0" marginwidth="0" src="'+ finalURL +'" name="fbxFrame" id="ltdIf" style="display:none';

		if (this.settings.width)
			divc += '; width:'+ this.settings.width +'px; height:'+ this.settings.height +'px';

		this.settings.html = divc +'"><\/iframe><\/div>';
		return this;
	},

	// Set the Dialog HTML
	setSize: function(newWidth, newHeight) {
		if (newWidth)  this.settings.width  = newWidth;
		if (newHeight) this.settings.height = newHeight;
		return this;
	},

	/* Private Members */

	// Default Settings
	settings: {
		url			: null,			// URL to launch instead of static HTML
		preloadUrl	: false,		// Whether to preload the URL after a dialog closes. Useful if dialog navigates away from initial URL.
		html		: '',			// Static HTML instead of URL
		modal		: 'true',		// Modal window
		title		: 'LinkTix',	// Dialog title
		width		: 600,			// Dialog width
		height		: 400,			// Dialog height
		posX		: 'center',		// X position of the dialog. 'center' keyword will force centering.
		posY		: 'center',		// Y position of the dialog. 'center' keyword will force centering.
		animate		: null,			// Type of animation to use.
									// Suppported:	    null  = No anmications
									//				'leftIn'  = Animate from Left
		callBack	: null			// Optional callback function after init() is called
	},

	dialogWindow	: null, 		// Current dialog object


	/* Private Methods */

	// Init Method. Called automatically by the Constructor when a new instance of LTDialog is created.
	init: function(options) {
		// Apply any options that override the defaults
        if(options) {
            $.extend(this.settings, options);
        }

		var dInstance = this;

/*	TODO: Apply positioning / sizing and other settings, Animations and Ajax URL support using Facebox

				var posX = settings.posX;
				var posY = settings.posY;
				var width = settings.width;
				var height = settings.height;
				var animation = settings.animate;

				if (posX == 'center')
					posX = Math.floor(parseInt($(window).width() / 2) - parseInt(width) / 2);

				if (posY == 'center')
					posY = Math.floor(parseInt($(window).height() / 2) - parseInt(height) / 2);

				if (animation == 'leftIn') {
			        dialog.css({
			            width: width,
			            height: height,
			            top: posY,
			            left: -10-width,
			            marginLeft: 0
			        })
			        .jqmShow()
			        .animate({
			            width: width,
			            height: height,
			            top: posY,
			            left: posX
			            }, 'slow');
				}
				else {
					dialog.css({
			            width: width,
			            height: height,
			            top: posY,
			            left: posX,
			            marginLeft: 0
					})
					.jqmShow()
					.fadeIn('slow');
				}
		    }
		});

		// The first time, we always preload the Dialog URL if it's provided
		var dialogURL = this.settings.url;
		if (dialogURL)
			setTimeout(function() { $('#jqmContent').attr('src', dialogURL) }, 200);
*/
		if (this.settings.callBack)
			this.settings.callBack();
	}
};

/****************************
 * LinkTix jQuery Extentions
 ****************************/

if (jQuery) {
	$.fn.extend({
		// ltHelperText adds Helper mouse-over text for links. Supports variable substitutions.
		ltHelperText: function(txt) {
			this.each(function() {
				var el = $(this);
				if (!el.attr("title"))
					el.attr("title", txt.replace(/<lbl>/,el.text().replace(/^[\s\t]+/,'').replace(/[\r\n]/g,'')));
			});
		}
	});
}

/****************************
 * LinkTix Utility Functions
 ****************************/

var oauthWindow=null;

// Launch an OAuth Authorization Window to request access from the user to an external API
function getOAuthAccess(type, extraParams) {
	// NOTE: When the OAuth authorization succeeds, it will call "oauthCallback()" from the parent window (the one that called getOAuthAccess)
	oauthWindow = window.open('/linktix/v3/ltOAuth.html?isfb=0&vn=v3/ltOAuth&type='+ type + (extraParams ? ('&'+ extraParams) : ''),'ltOAuthWindow','width=780,height=550');
}

// Close the OAuth Window
function closeOAuthWindow() {
	if (oauthWindow)
		oauthWindow.close();

	// Focus on this window
	if (window.focus)
		window.focus();
}

// Prepare Facebox and invoke it
function launchFacebox(content) {
	if ($.facebox.settings.forceY) {
		$.facebox.settings.boxPosY = $.facebox.settings.forceY;
	}
	else {
		// If the event click shows a mouse position below 400 pixels, we align the box close to the mouse
		var topPos = (lastMouseY > 200) ? (lastMouseY - 80) : null;
		var winHeight = $(window).height();
		if ((winHeight - topPos) < 400 && location.search.indexOf('isfb=0') < 0)
			topPos = winHeight - 500;	// Ensure that the box doesn't get cut-off at the bottom
		$.facebox.settings.boxPosY = topPos;
	}

	// Launch it
	$.facebox(content);
}

// Replacement for standard alert()
function ltAlert(msg, title) {
	// IE 5- will not work so we use a standard alert for it
	if (!jQuery.browser.msie || navigator.userAgent.search(/MSIE [67891]/) > 0)
		launchFacebox(msg);
	else
		alert(msg);
}

// Replacement for standard confirm() {
function ltConfirm(msg, title, yesLabel, yesCallback, noLabel, noCallback) {
	// IE 5- will not work so we use a standard alert for it
	if (!jQuery.browser.msie || navigator.userAgent.search(/MSIE [67891]/) > 0) {
		$.facebox.settings.buttons = [{label:'Yes', callBack: function() { if (yesCallback) yesCallback(); }},
							  		 {label: 'No', callBack: function() { if (noCallback) noCallback(); }}];

		launchFacebox(msg);
		return 'wait';			// Indicates that the callbacks will take care of things
	}
	else
		return confirm(msg);	// Returns true / false, indicating that this is the standard confirm()
}

// Standard Error Box Display
//  title is optional
function ltShowError(msg, title) {
	var errorBox = $('.errorbox');
	if (!errorBox.length) {
		ltAlert(msg, title); // No errorbox DIV found, so we use the pop-up
		return;
	}

	if (title)
		$('.errorbox-head').text(title);

	$('.errorbox-message').html(msg);

	errorBox
		.eq(0)
		.slideDown('fast');
}

// Hide the existing error box
function ltHideError() {
	var errorBox = $('.errorbox');
	if (errorBox.length)
		errorBox.eq(0).fadeOut('fast');
}

// In Date fields, fix years that have 2 digits or less
function ltFixDateYear( fieldSelector ) {
	var field = $(fieldSelector);
	var dateStr = field.val();
	var twoDigitYear = dateStr.match(/^(.+)\/(\d\d?)$/);
	if (twoDigitYear) {
		// Make assumption on the century
		var yr = twoDigitYear[2];
		if (yr.length < 2)
			yr = '0'+ yr;

		if (parseInt(yr) > 50)
			yr = '19'+ yr;
		else
			yr = '20'+ yr;

		field.val ( twoDigitYear[1] +'/'+ yr );
	}
	return field.val();
}

// Highlight an object for a few seconds then fade the highlighting away
function ltHighlightFade(obj, delay) {
	var objPos = obj.position();
	if (!objPos) return;
	var backupBG = obj.css("backgroundColor");
	var hiObj = obj
				.css({ backgroundColor: "transparent", zIndex: 2 })
				.clone()
				.css({	position: "absolute",
						backgroundImage: "none",
						zIndex: 1,
						left: objPos.left,
						top: objPos.top,
						width: obj.width(),
						height: obj.height(),
						backgroundColor: "#FAEEBB"
				})
				.hide()
				.appendTo("body")
				.fadeIn(200);

	// Try to "scroll to" this object by doing a focus an any <A> links enclosed
	var lastLink = obj.find('a:last');
	var llo = (lastLink.length) ? lastLink.get(0) : null;
	if (llo) {
		try {
			setTimeout(function() {
				llo.focus();
				llo.blur();
			}, 800);		// Wait .8 seconds in case other processing is underway (like frame resizing)
		}
		catch(e){}
	}

	// Remove the highlighting after a couple of seconds
	setTimeout(function() {
		hiObj.fadeOut(1500, function() {
			$(this).remove();
			obj.css({ backgroundColor: backupBG })
		});
	}, delay);
}

// For Menu Pages - Reset all menu items to unselected
function resetMenu() {
	$('.menuitem').removeClass("menuactive");
}

// For Menu Pages - Set a specific menu as active
function setMenu(newLink) {
	// Avoid re-setting the current menu
	if (currMenu != newLink) {
		resetMenu();

		if (newLink) {
			newLink.parent().addClass("menuactive");
			currMenu = newLink;
			newLink.blur();
		}
	}
}

// Display the Contact Us Dialog
function popContactUs(win) {

	// On mobile browser, let the calling frame call it's own version to prevent UI bleedthrough (on iPhone)
	if (win && isMobile() && win.popContactUs) {
		win.popContactUs();
		return;
	}

	var html = '<div id="formBox"><b class="appTitle">Contact LinkTix</b><br><br>'
				+'<form id="contactUsForm" method="post" action="javascript:sendMail()">'
				+'<input type="hidden" name="attn" value="Contact from LinkTix.com" />'
				+'Name: <br><input type="text" name="visitor" size="35" maxlength="50" /><br><br>'
				+'Email Address:<br><input type="text" name="visitormail" size="35" maxlength="40" /><br><br><br>'
				+'Message:<br><textarea name="notes" rows="3" cols="40" onchange="this.value=this.value.substr(0,1000)"></textarea><br>'
				+'<center><input class="inputbutton" type="submit" value="Send" /></center></form><br><br><br><br></div>';

	$.facebox.settings.boxWidth = 350;
	launchFacebox(html);
}

// Sends a Contact Us e-mail using a PHP script
function sendMail() {
	var postData = $("#contactUsForm").serialize() +"&done=true";
	if (postData.search(/=&/) > 0) {
		alert("Please fill out all fields");
		return;
	}

	$.ajax({
		type: "POST",
		url: "/send.php",
		data: postData,
		success: function(str, postStatus) {
			if (postStatus == "success") {
				$("#formBox").hide();
				$.facebox.reveal('<br><br><br><p align="center" class="basic-heading"><b>Thank You!</b></p><br><br><br>');
				setTimeout( $.facebox.close, 2000 );
			}
			else {
				alert("Error: "+ str, "Unable to send e-mail");
			}
		},
		error: function(xhr, postStatus, theError) {
			alert("Error ("+ ((theError) ? theError : xhr.status)  +") : "+ ((xhr && xhr.statusText) ? xhr.statusText : "Error"),
					"Unable to send e-mail");
		}
	});
}

// Displays the 100% Guarantee Dialog
function showGuarantee(win) {

	// On mobile browser, let the calling frame call it's own version to prevent UI bleedthrough (on iPhone)
	if (win && isMobile() && win.showGuarantee) {
		win.showGuarantee();
		return;
	}

	var html = '<div class="dialog-heading calign" style="padding-bottom: 20px">100% Guarantee</div>'
				+'<div class="basic-content calign" style="padding-bottom: 20px">Trusted Merchants that participate in the LinkTix Trusted Ticket Exchange will not charge a customer until their order can be 100% guaranteed. This means you are not charged for any tickets unless your order can be successfully filled. You will be notified about orders that cannot be completed as initially placed, and your credit card will not be charged for those orders.</div>';

	$.facebox.settings.boxWidth = 375;
	launchFacebox(html);
}

// Displays the Ticket Exchange Dialog
function showTicketExchange() {
	var html = '<div class="dialog-heading calign" style="padding-bottom: 20px">LinkTix Trusted Ticket Exchange</div>'
				+'<div class="basic-content calign" style="padding-bottom: 20px">LinkTix connects you with millions of available tickets from 1,000\'s of trusted ticket brokers on the LinkTix Trusted Ticket Exchange. '
				+'In partnership with the <a class="link" target="_extSite" href="http://corporate.ticketnetwork.com/">Ticket Network</a> '
				+'and its Trusted Merchant program, LinkTix provides a <a class="link" onclick="showGuarantee()" href="javascript://">100% Guarantee</a> on ticket transactions.</div>';

	$.facebox.settings.boxWidth = 320;
	launchFacebox(html);
}

// Displays the Settings page in a Dialog
function showSettings() {
	if (!window.ltDialog) {
		window.ltDialog = new LTDialog({
			html: '',		  			// Will be filled when each dialog is opened
			title: "LinkTix Settings"	// Will be overriden when each dialog is opened
		});
	}

	$.facebox.settings.forceY = 70;

	ltDialog
		.setSize(580,520)
		.setURL('/linktix/settings.html'+ location.search +'&dlg=1', true)
		.setTitle("LinkTix Settings")
		.showDialog();

	$('#facebox .body').css({ backgroundColor: "#FFF" });
}

// Displays the Email Import page in a Dialog
function showEmailImport() {
	if (!window.ltDialog) {
		window.ltDialog = new LTDialog({
			html: '',		  			// Will be filled when each dialog is opened
			title: "Import Email Contacts"	// Will be overriden when each dialog is opened
		});
	}

	$.facebox.settings.forceY = 170;

	ltDialog
		.setSize(550,400)
		.setURL('/linktix/v3/ltEmailImport.html'+ (location.search ? (location.search +'&') : '?') +'dlg=1', true)
		.setTitle("Import Email Contacts")
		.showDialog();

	$('#facebox .body').css({ backgroundColor: "#FFF" });
}

// Whether the browser is a mobile version
//   NOTE: For now we check only for iPhone / iPod Touch. As mobile browsers catch-up (Opera mobile, WebOS, Mobile Firefox) we should add them.
function isMobile() {
	var agent = navigator.userAgent;
	return agent.match(/iPhone/i);
}

// Function to rotate ads
var numAds = 3;
var currAd = 0;
function rotateAds() {
	currAd++;
	if (currAd > numAds) currAd = 1;
	var strAd = (currAd < 10) ? ('0'+currAd) : (''+currAd);

	$("#adImg").fadeOut('slow', function() { $(this).attr("src", "/linktix/static/images/ltad"+ strAd +".jpg").fadeIn('slow') });
	window.adTimer = setTimeout(rotateAds, 10000);
}

// Prepare & preload the ads
function prepareAds() {
	var cacheImg = new Array(numAds);
	for(var i=1; i <= numAds; i++) {
		var strAd = (i < 10) ? '0'+i : ''+i;
		cacheImg[i] = new Image();
		cacheImg[i].src = "static/images/ltad"+ strAd +".jpg"
	}
	rotateAds();
}

// Escape unsafe characters for HTML
function safeHTML(str) {
	return str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

// Determines if a JSON response is safe to eval(). Prevents malicious JS injection.
function isSafeJSON(text) {
	return (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, '')));

	/* Alternative method. In case the above doesn't always work.
	var str = text.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str)
    */
}

// This function unescapes special HTML characters and returns the decoded string
var unescapeHtml = function (html) {
	var result = "";
	var temp = document.createElement("div");
	temp.innerHTML = html.replace(/<\/?[^>]+>/gi, '');
	for(var i=0; i < temp.childNodes.length; i++) {
		result += temp.childNodes[i].nodeValue;
	}
	temp.removeChild(temp.firstChild)
	return result;
}

// Creates a Regular Expression based on user input
function createRegExp(input) {
	// Escape special characters that interfere with the RegExp
	var fixedInput = input.replace(/[\[\"]/g,'') // Some characters need to be removed altogether
						  .replace(/[\$\\\^\*\(\)\+\[\]\?]/g, '\\$1');
	return (new RegExp(fixedInput, 'i'));
}

// Pads a string (str) on the left to a given length (len) using a specific character (charToPrepend)
function leftPad(str, charToPrepend, len) {
    return (new Array(len - (''+str).length + 1)).join(charToPrepend).concat(str);
}

// Go back to the previous page
function loadPreviousPage() {
	parent.showLoading();
	history.back(-1);
}

// Used by ltDataSaved.jsp to support various specific processing based on the page that succesfully submitted
function processFormSuccess() {
	var cf = document.referrer;
	
	// Check for the ltMessage.jsp form
	var doClose = (cf && cf.match(/\/ltMessage\.html\?.*evid=([0-9,]+)/));
	if (doClose) {
		// Process any server-side errors.
		if (ltFormManager.processSSErrors()) {
			return;
		};

		if (parent && parent.parent)
	    	parent.parent.pageLoad(null, "/linktix/v3/ltEditHave.html?evid="+ doClose[1]);
	    return;
	}
}

/****************************
 * Generic Initialization
 ****************************/

// Mouse Position
var lastMouseX, lastMouseY;

// Auto-Load for all pages
$(document).ready( function() {
	// Hide the Loading display if any
	// Assumes that any screen that gets loaded with a Loading display will include ltUtils.js
	var urlPathName = location.pathname;
	window.isLtLoading = true;
	if (window.hideLoading)
		setTimeout(	window.hideLoading, 500);

	// When we are in a dialog, hide the parent spinner
	if (location.search.search(/[\?\&]indl=1/) >= 0 && parent.$) {
		parent.$("#ltdSp").hide();
		parent.$("#ltdIf").fadeIn(500);
	}

	// Generic UI Click Events
	$("a#helpLink").click(function() { parent.pageLoad(null, '/linktix/static/about/howItWorks.htm'); });
	$("a#contactLink").click( parent.popContactUs );
	$("img#homepageLogo").click( function() { 
		if (window.isFBConnect && window.pageLoad) 
			pageLoad(null, '/linktix/v3/ltMyTix.html');
		else {
			location.href = location.href;
		}
	});
	$("a#homeLink").click( function() { 
		if (parent.isFBConnect && parent.pageLoad) 
			parent.pageLoad(null, '/linktix/v3/ltMyTix.html');
		else {
			parent.location.href = parent.location.href;
		}
	});

	// Populate the URL of links with descriptive text for the status bar
	if (!$.browser.webkit) {
		$("a").each( function() { 
			var o = this;
			if (o.href == 'javascript://') {
				var ot=$(this).text();
				if (!ot)
					ot = o.title;
		
				if (ot)
					o.href = 'javascript://   '+ $.trim(ot);
			}
		});
	}

	// Keep Track of the mouse position for pages that need this information (like positioning dialog boxes)
	$().mousemove(function(e) {
		lastMouseX = e.pageX;
		lastMouseY = e.pageY;
	});

	// Cleanup on unload
	var currentUnload = window.onunload;
	window.onunload = function() {
		if (currentUnload)
			currentUnload();

		// Delete large objects
		delete ltPagination;
		delete ltFormManager;
	};
});

// Supersleight jQuery Plugin for Transparent PNGs in IE6 (by Drew McLellan)
// http://allinthehead.com/retro/338/supersleight-jquery-plugin
jQuery.fn.supersleight = function(settings) {
	settings = jQuery.extend({
		imgs: true,
		backgrounds: true,
		shim: '/linktix/static/images/shim.gif',
		apply_positioning: true
	}, settings);
	
	return this.each(function(){
		if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 7 && parseInt(jQuery.browser.version, 10) > 4) {
			jQuery(this).find('*').andSelf().each(function(i,obj) {
				var self = jQuery(obj);
				// background pngs
				if (settings.backgrounds && self.css('background-image').match(/\.png/i) !== null) {
					var bg = self.css('background-image');
					var src = bg.substring(5,bg.length-2);
					var mode = (self.css('background-repeat') == 'no-repeat' ? 'crop' : 'scale');
					var styles = {
						'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "', sizingMethod='" + mode + "')",
						'background-image': 'url('+settings.shim+')'
					};
					self.css(styles);
				};
				// image elements
				if (settings.imgs && self.is('img[src$=png]')){
					var styles = {
						'width': self.width() + 'px',
						'height': self.height() + 'px',
						'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + self.attr('src') + "', sizingMethod='scale')"
					};
					self.css(styles).attr('src', settings.shim);
				};
				// apply position to 'active' elements
				if (settings.apply_positioning && self.is('a, input') && (self.css('position') === '' || self.css('position') == 'static')){
					self.css('position', 'relative');
				};
			});
		};
	});
};

