﻿











/************************************************************************
*				string.js				*
*			Custom String methods				*
************************************************************************/





// Reverse the string ( 'Hi how are you' => 'uoy era woh iH' )
String.prototype.reverse = function()
{
	var str = '';
	for ( var i=this.length-1; i>=0; i-- )
		str += this.charAt(i);
	return str;
};





// Like Array.splice: insert a string while deleting a number of characters
String.prototype.splice = function( index, len, str )
{
	return this.substr( 0, index ) + str + this.substr( index + len );
};





// At a given index, insert a string
// Like splice(), but nothing is removed
String.prototype.insert = function( index, str )
{
	return this.substr( 0, index ) + str + this.substr( index );
};





// Trim whitespace from the front and back
String.prototype.trim = function()
{
	return this.replace( /$\s+|\s+^/g, '' );
};





// Repeat a string N times
String.prototype.repeat = function( N )
{
	return new Array( N + 1 ).join( this );
};












/************************************************************************
*				params.js				*
*			Manipulating URL GET parameters			*
************************************************************************/





// Return GET parameters from URL as an Object
function getParams()
{
	var params = new Object();
	var url = document.location.href;
	
	// If '?' isn't found, no need to proceed
	if( url.indexOf('?') != -1 )
	{
		// Get url, split into 'name=value' pairs
		var paramList = url.split('?')[1];
		paramList = paramList.split('&');
		
		for( var i=0; i<paramList.length; i++ )
		{
			// Split into [ name, value ] array
			var temp = paramList[i].split("=");
			
			// urlDecode name and value
			for( var j=0; j<2; j++ )
				temp[j] = decodeURIComponent( temp[j] );
			
			// Add to params along with lowercase version
			params[temp[0]] = temp[1];
			params[temp[0].toLowerCase()] = temp[1];
		}
	}
	
	return params;
}











/************************************************************************
*				csv.js					*
*			Functions for parsing CSV			*
************************************************************************/





// Parse one row of CSV into an array of fields
function parseCsvRecord( record )
{
	/*
	**	CSV files from Excel use linebreaks to separate rows and
	**	commas to separate columns within a row.
	**	
	**	Cells that contain commas (,) or quotation marks (") are
	**	enclosed by quotes.
	**	
	**	Quotation marks inside a cell are replaced with double-
	**	double quotes ("").
	**	
	**	We will develop a custom escape code for commas so that
	**	structural commas are distinct from commas which are part
	**	of the document content.
	**	
	**	We will also remove structural quotes while returning
	**	double-double quotes in the document content to their
	**	original single-double form.
	**	
	**	We will then be able to split the string along structural
	**	commas and unescape content commas.
	*/
	
	// Custom escapes for commas and quotes
	// ,, => structural comma
	// ,c => document content comma
	// ,q => document content quote ("")
	
	// Escape commas with the assumption that all are structural
	record = record.replace( /,/g, ',,' );
	
	// Any point in the string with an odd number of quotes behind it
	// must be inside a record.
	// Use this to change content commas to ,c instead of ,,
	var insideQuotes = false;
	for( var j=0; j<record.length; j++ )
	{
		if( record.charAt(j) == '"' )
			insideQuotes = !insideQuotes;
		
		// If the beginning of a comma escape code is found
		else if( record.charAt(j)==',' )
		{
			// Check the next character
			j++;
			if( insideQuotes )
				record = record.splice( j, 1, 'c' );
		}
	}
	
	// Now that the commas are escaped, do the quotes
	
	record = record.replace( /""/g, ',q' );		// Escape double (content) quotes
	record = record.replace( /"/g, '' );		// Eliminate single (structural) quotes
	record = record.replace( /,q/g, '"' );		// Unescape double quotes to single
	
	// Now we can split the string and unescape content commas
	record = record.split(',,');
	for( var j=0; j<record.length; j++ )
		record[j] = record[j].replace( /,c/g, ',' );
	
	return record;
}





// Parse an entire CSV document into a matrix, making use of parseCsvRecord
function parseCsvDoc( str )
{
	// dos2unix and split into an array of records
	var arr = str.replace( /\r/g, '\n' ).split('\n');
	
	for( var i=0; i<arr.length; i++ )
	{
		// If no non-whitespace characters are found
		if( !arr[i].match(/\S/) )
		{
			// Line is empty, so excise it
			arr.splice( i, 1 );
			i--;
		}
		else
		{
			// Parse the record into an array and replace it
			arr[i] = parseCsvRecord( arr[i] );
		}
	}
	
	return arr;
}











/************************************************************************
*				ajax.js					*
*			All-purpose Ajax function			*
************************************************************************/





// Retrieve the given URL and then run the given function (no arguments)
// Could probably allow arguments using the arguments property
// http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Functions:arguments
function getDocAndDo( url, func )
{
	var httpRequest;
	
	
	/*
	if (window.XMLHttpRequest)	// Mozilla, Safari, ...
	{
		httpRequest = new XMLHttpRequest();
	}
	else if (window.ActiveXObject)	// IE
	{
		try
		{
			httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch (e)
		{
			try
			{
				httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
			}
			catch (e) {}
		}
	}
	
	if (!httpRequest)
	{
		alert('Sorry, the course listing can\'t be displayed because your browser does not support Ajax.');
		return false;
	}
	*/
	
	try
	{
		// Firefox, Opera 8.0+, Safari
		httpRequest = new XMLHttpRequest();
	}
	catch (e)
	{
		// Internet Explorer
		try
		{
			httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch (e)
		{
			try
			{
				httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
			}
			catch( e )
			{
				alert('Sorry, the course listing can\'t be displayed because your browser does not support Ajax. For best results, use Internet Explorer 6 or later.');
				return false;
			}
		}
	}
	
	httpRequest.onreadystatechange = function()
	{
		if (httpRequest.readyState == 4)
		{
	//		if(httpRequest.status == 200)
	//		{
				func( httpRequest );
	//		}
	//		else
	//		{
	//			alert('There was a problem with the request.');
	//		}
		}
	};
	
	httpRequest.open( 'GET', url, true );
	httpRequest.send('');
}











/************************************************************************
*				sortable.js				*
*			Implement sortable HTML tables			*
************************************************************************/





// An array of arrays of functions
// Each row corresponds to a sortable table
// Each cell corresponds to a field in that table
// Each cell has its own function to sort the table
// Each function calls the function sortTableInternal using its indices as arguments
var sortTable = new Array();





// Generally useful function which returns an element's class information as an array
// Note that a DOM class is distinct from a training class
function getDOMclass( elm )
{
	var str = elm.className;
	str = str.replace( /\s+/g, ' ' );
	var classes = str.split(' ');
	return classes;
}





// Another general function used to add event listeners
function addEvent( elm, evType, func, useCapture )
// addEvent and removeEvent
// cross-browser event handling for IE5+, NS6 and Mozilla
// By Scott Andrew
{
	if (elm.addEventListener){
		elm.addEventListener( evType, func, useCapture );
		return true;
	} else if (elm.attachEvent){
		var r = elm.attachEvent( "on"+evType, func );
		return r;
	} else {
		alert("Handler could not be added.");
	}
}





// Return a list of all tables designated sortable
// if elm != null, returns only those under elm
function sortableTableList( elm )
{
	if( elm == null )	elm = document.getElementsByTagName('body')[0];
	
	var allTables = elm.getElementsByTagName('table');
	var sortableTables = new Array();
	
	for( var i=0; i<allTables.length; i++ )
	{
		var curr = allTables[i];
		var currClasses = getDOMclass( curr );
		var isSortable = false;
		for( var j=0; j<currClasses.length && !isSortable; j++ )
			if( currClasses[j] == 'sortable' )
				isSortable = true;
		if( isSortable )
			sortableTables.push( curr );
	}
	
	return sortableTables;
}





// Makes sure DOM classes sortable-even and sortable-odd are correctly applied to the table
// Used for coloring
function recolorRows( sortableTable )
{
	var rows = sortableTable.getElementsByTagName('tr');
	var even = true;
	
	for( var i=0; i<rows.length; i++ )
	{
		if( rows[i].parentNode.tagName.toLowerCase() != 'tbody' ) continue;
		
		var str = rows[i].className;
		str = str.replace( /sortable-even|sortable-odd/g, '' );
		str = str.replace( /\s+/g, ' ' );
		str = str.replace( /$ | ^/g, '' );
		
		if( str != '' )
			str += ' ';
		
		if( even )
			str += 'sortable-even';
		else
			str += 'sortable-odd';
		
		even = !even;
		rows[i].className = str;
	}
}





// Perform certain operations to initiate sortable tables upon page load
function initSortableTables()
{
	var allSortableTables = sortableTableList();
	
	// Make space for the sort functions
	window.sortTable = new Array( allSortableTables.length );
	
	for( var i=0; i<allSortableTables.length; i++ )
	{
		var curr = allSortableTables[i];
		
		//////////////////////////
		var allCells = curr.getElementsByTagName('td');
		var headerCells = new Array();
		for( var j=0; j<allCells.length; j++ )
			if( allCells[j].parentNode.parentNode.tagName.toLowerCase() == 'thead' )
				headerCells.push( allCells[j] );
		//////////////////////////
		
		// New sort functions for each of the header cells
		window.sortTable[i] = new Array( headerCells.length );
		
		for( var j=0; j<headerCells.length; j++ )
		{
			// Check if the column has been marked unsortable
			var cellClasses = getDOMclass( headerCells[j] );
			var columnIsSortable = true;
			for( var k=0; k<cellClasses.length; k++ )
				if( cellClasses[k] == 'sortable-nosort' )
					columnIsSortable = false;
			if( !columnIsSortable )
				continue;
			
			// It hasn't, so add that function
			var str = 'sortTableInternal( sortableTableList()[' + i + '],' + j + ')';
			window.sortTable[i][j] = new Function( str );
			
			// Now add a listener to run that function
			addEvent( headerCells[j], 'click', window.sortTable[i][j], true );
			
			// While we're here, add the sort icons
			var icon = document.createElement('img');
			icon.border = '0';
			icon.src = iconURLs.unsorted;
			headerCells[j].insertBefore( icon, headerCells[j].firstChild );
		}
		
		recolorRows( curr );
	}
}





// Sort the provided table using the field indicated by fieldIndex as the criterion
// Called internal because it's called automatically by sortTable[i][j]
function sortTableInternal( table, fieldIndex )
{
	
	
	//////////////////LOCAL FUNCTIONS/////////////////
	
	
	
	// Returns true if array[index1] belongs before array[index2]
	// array is an array of arrays of data
	// index1 and index2 are int indices
	// fieldIndex and isAscending are defined above the scope of this function
	var ordered = function( array, index1, index2 )
	{
		var value1 = array[index1][fieldIndex];
		var value2 = array[index2][fieldIndex];
		
		if( value1 == value2 )
			return ( index1 < index2 );
		else
		{
			if( isDate )
			{
				// Convert to Date type before comparing
				value1 = new Date( value1 );
				value2 = new Date( value2 );
			}
			return ( (value1<value2) == (isAscending) );
		}
	};
	
	
	
	/////////////////////////////////////////////////
	
	
	
	// Standard quicksort implementation
	var quicksort = function( array )
	{
		if( array.length <= 1 )
			return array;
		
		var pivotIndex = Math.floor( array.length * Math.random() );
		var before = new Array();
		var after = new Array();
		
		for( var i=0; i<array.length; i++ )
		{
			if( i == pivotIndex )	continue;
			
			if(ordered( array, i, pivotIndex ))
				before.push( array[i] );
			else
				after.push( array[i] );
		}
		
		before = quicksort( before );
		after = quicksort( after );
		
		return [].concat( before, [array[pivotIndex]], after );
	};
	
	
	
	////////////////////////////////////////////////
	
	
	
	
	
	// Sort table along field number fIndex
	
	
	// Get all the rows of data into an array called rows
	var allRows = table.getElementsByTagName('tr');
	var bodyRows = new Array();
	for( var i=0; i<allRows.length; i++ )
		if( allRows[i].parentNode.tagName.toLowerCase() == 'tbody' )
			bodyRows.push( allRows[i] );
	
	
	// Get the header cells into an array
	var allCells = table.getElementsByTagName('td');
	var headerCells = new Array();
	for( var i=0; i<allCells.length; i++ )
		if( allCells[i].parentNode.parentNode.tagName.toLowerCase() == 'thead' )
			headerCells.push( allCells[i] );
	
	
	// Define isAscending and isDate
	var classes = getDOMclass( headerCells[fieldIndex] );
	var isAscending = true;
	var isDate = false;
	for( var i=0; i<classes.length; i++ )
	{
		if( classes[i] == 'sortable-ascending' )
			isAscending = false;
		else if( classes[i] == 'sortable-datefield' )
			isDate = true;
	}
	
	
	// Make appropriate changes to the class names and icon srcs of the (sortable) header cells
	for( var i=0; i<headerCells.length; i++ )
	{
		var str = headerCells[i].className;
		str = str.replace( /sortable-ascending|sortable-descending/g, '' );
		str = str.replace( /\s+/g, ' ' )
		str = str.replace( /$ | ^/g, '' );
		headerCells[i].className = str;
		
		if( headerCells[i].getElementsByTagName('img')[0] )
			headerCells[i].getElementsByTagName('img')[0].src = iconURLs.unsorted;
	}
	
	var str = headerCells[fieldIndex].className;
	if( str.length > 0 )
		str += ' ';
	
	if( isAscending )
	{
		headerCells[fieldIndex].className = str + 'sortable-ascending';
		headerCells[fieldIndex].getElementsByTagName('img')[0].src = iconURLs.sortedAscending;
	}
	else
	{
		headerCells[fieldIndex].className = str + 'sortable-descending';
		headerCells[fieldIndex].getElementsByTagName('img')[0].src = iconURLs.sortedDescending;
	}
	
	
	// Copy DATA ONLY into an array called data
	var data = new Array( bodyRows.length );
	for( var i=0; i<bodyRows.length; i++ )
	{
		var cells = bodyRows[i].getElementsByTagName('td');
		data[i] = new Array( cells.length + 1 );
		for( var j=0; j<cells.length; j++ )
			data[i][j] = cells[j].innerHTML.trim();
		data[i][cells.length] = bodyRows[i].className;
	}
	
	
	// Sort!
	data = quicksort( data );
	
	
	// Display
	for( var i=0; i<data.length; i++ )
	{
		var curr = bodyRows[i].getElementsByTagName('td');
		
		for( var j=0; j<data[i].length-1; j++ )
			curr[j].innerHTML = data[i][j];
		bodyRows[i].className = data[i][data[i].length-1];
	}
	
	
	// Recolor
	recolorRows( table );
	
}











/************************************************************************
*				Misc					*
*		Use the parsed CSV data to construct the page		*
************************************************************************/





// Arrange the contents of an array of arrays into an array of hashes
// This is what has to be rewritten if the document structure changes
function getClasses( matrix )
{
	var arr = new Array( matrix.length-1 );
	
	for( var i=1; i<matrix.length; i++ )
	{
		var curr = matrix[i];
		var obj = new Object();
		
		// The following assumes a certain structure
		obj.courseCode = curr[0];
		obj.courseName = curr[1];
		obj.location = curr[2];
		obj.startDate = curr[3];
		obj.endDate = curr[4];
		obj.website = curr[5];
		obj.carusa = curr[6];
		obj.carint = curr[7];
		obj.dist = curr[8];
		obj.fad = curr[9];
		obj.genreg = curr[10];
		obj.notes = curr[11];
		
		arr[i-1] = obj;
	}
	
	return arr;
}





// Display the classes as HTML
function displayListing( httpRequest )
{
	// The Ajax connection has been made and the text returned
	// Parse the document and put it into an Object called listing
	var listing = getClasses( parseCsvDoc( httpRequest.responseText ) );
	
	// Before we start, add any and all relevant GET parameters to a query string
	var queryString = '';
	var params = getParams();
	var queryFields = [ 'em', 'fn', 'ln', 'cmp', 'add', 'cty', 'st', 'zip', 'ph', 'dst' ];
	
	for( var i=0; i<queryFields.length; i++ )
	{
		var curr = queryFields[i];
		if( typeof params[curr] != 'undefined' )
			queryString += '&' + curr + '=' + encodeURIComponent( params[curr] );
	}
	
	// Get a pointer to the table which will display our data
	var table = document.getElementById('class-listing');
	
	var out = '';
	
	table.border = '0';
	table.cellSpacing = '0';
	table.cellPadding = '0';
	table.className = 'sortable';
	
	// Add the header row
	
	var thead = document.createElement('thead');
	thead.appendChild( document.createElement('tr') );
	
	var fields = [ 'Code', 'Course Title', 'Location', 'Start', 'End', 'More info' ];
	var isSortable = [ true, true, true, true, true, false ];
	var isLink = [ false, false, false, false, false, true ];
	var isDate = [ false, false, false, true, true, false ];
	
	for( var i=0; i<fields.length; i++ )
	{
		var temp = document.createElement('td');
		temp.title = ((isSortable[i])?('Sort'):('Cannot sort')) + ' by ' + fields[i];
		
		temp.className = '';
		if( !isSortable[i] )
			temp.className += ' sortable-nosort';
		if( isLink[i] )
			temp.className += ' link';
		if( isDate[i] )
			temp.className += ' sortable-datefield';
		temp.className = temp.className.trim();
		
		temp.appendChild( document.createTextNode( fields[i] ) );
		thead.firstChild.appendChild( temp );
	}
	
	table.appendChild( thead );
	
	// Add the body data
	
	var tbody = document.createElement('tbody');
	for( var i=0; i<listing.length; i++ )
	{
		
		var curr = listing[i];
		var row = document.createElement('tr');
		
		// Course code
		var cell = document.createElement('td');
		if( curr.courseCode.indexOf('*') > -1 )
		{
			row.className += ' confirmed-class';
			curr.courseCode = curr.courseCode.replace( /\*/g, '' );
		}
		else if( curr.courseCode.indexOf('.') > -1 )
		{
			row.className += ' sold-out-class';
			curr.courseCode = curr.courseCode.replace( /\./g, '' );
		}
		cell.appendChild( document.createTextNode( curr.courseCode ) );
		row.appendChild( cell );
		
		// Course name
		var cell = document.createElement('td');
		cell.appendChild( document.createTextNode( curr.courseName ) );
		row.appendChild( cell );
		
		// Location
		var cell = document.createElement('td');
		cell.appendChild( document.createTextNode( curr.location ) );
		row.appendChild( cell );
		
		// Start date
		var cell = document.createElement('td');
		cell.appendChild( document.createTextNode( curr.startDate ) );
		row.appendChild( cell );
		
		// End date
		var cell = document.createElement('td');
		cell.appendChild( document.createTextNode( curr.endDate ) );
		row.appendChild( cell );
		
		// More info
		var cell = document.createElement('td');
		cell.className = 'link';
		
		// As long as curr.website is not empty (excluding whitespace)
		if( curr.website.match(/\S/) )
		{
			var detailsLink = document.createElement('a');
			detailsLink.className = 'detailsLink';
			detailsLink.appendChild( document.createTextNode('[Details]') );
			detailsLink.href = curr.website;
			detailsLink.target = '_blank';
			
			cell.appendChild( detailsLink );
			cell.appendChild( document.createTextNode(' ') );	// Add a space in between
		}
		
		var regLink = document.createElement('a');
		regLink.className = 'regLink';
		regLink.appendChild( document.createTextNode('[Register]') );
		var params = getParams();
		if( params.typ )
		{
			switch( getParams().typ )
			{
				case '1':	regLink.href = curr.carusa;	break;
				case '2':	regLink.href = curr.carint;	break;
				case '3':	regLink.href = curr.dist;	break;
				case '4':	regLink.href = curr.fad;	break;
				default:	regLink.href = curr.genreg;	break;
			}
		}
		else
			regLink.href = curr.genreg;
		
		regLink.href = regLink.href.replace( /rTypeID/, 'RegTypeID' );
		regLink.href = regLink.href.replace( /\/\?/, '/Checkin.asp?' )
		regLink.href += queryString;
		
		cell.appendChild( regLink );
		
		row.appendChild( cell );
		
		// At last we can attach the row
		
		tbody.appendChild( row );
		
	}
	table.appendChild( tbody );
	
	// Make the table sortable	
	initSortableTables();
	
	// Get rid of that loading icon
	deleteFromDOM( document.getElementById('loading-icon') );
	mergeTextNodes();
	
	// Do preliminary sorting on the table
	// In reverse order of primacy
	sortTable[0][2]();	// First by location
	sortTable[0][0]();	// Then by code
	sortTable[0][3]();	// Finally by start date
}





// Helper function to remove a node
function deleteFromDOM( node )
{
	return node.parentNode.removeChild( node );
}





// Helps in debugging
function debug( obj )
{
	// Create debugger pane if it doesn't exist, then return a pointer to it
	while( (Debugger=document.getElementById('debugger')) == null )
	{
		Debugger = document.createElement('pre');
		Debugger.id = 'debugger';
		Debugger.style.border = 'solid 2px #00f';
		Debugger.style.backgroundColor = '#ffd';
		Debugger.style.margin = '1em';
		Debugger.style.padding = '1em';
		var body = document.getElementsByTagName('body')[0];
		body.insertBefore( Debugger, body.firstChild );
	}
	
	var tab = '\t';
	var linebreak = '<br />';
	
	var printToDebugger = function( subObj, depth )
	{
		switch( typeof( subObj ) )
		{
		case 'object':
			Debugger.innerHTML += ( subObj.length=='undefined' )?( 'Object' ):( 'Array( '+subObj.length+' )' ) + linebreak;
			Debugger.innerHTML += tab.repeat(depth) + '{' + linebreak;
			for( var i in subObj )
			{
				Debugger.innerHTML += tab.repeat(depth+1);
				Debugger.innerHTML += '[' + i + '] => ';
				printToDebugger( subObj[i], depth+2 );
			}
			Debugger.innerHTML += tab.repeat(depth) + '}' + linebreak.repeat(1);
			break;
		case 'string': case 'function': default:
			Debugger.innerHTML += subObj.toString().replace( /\n/g, linebreak + tab.repeat(depth) );
			Debugger.innerHTML += linebreak;
			break;
		}
	};
	
	printToDebugger( obj, 0 );
}





// Merge text nodes that have become separated through DOM manipulation
// Invisible, but makes me happy when debugging
// In spite of appearances, will take a DOM node as an argument
function mergeTextNodes()
{
	var elm = (arguments.length>0)?arguments[0]:document.getElementsByTagName('html')[0];
	if( !(elm.firstChild) )	return false;
	
	for( var i=0; i<elm.childNodes.length; i++ )
	{
		var curr = elm.childNodes[i];
		
		// Unless curr is the last sibling ...
		if( curr.nextSibling )
		{
			// And only if curr and its next sibling are both text ...
			if( curr.nodeType==curr.nextSibling.nodeType && curr.nodeType==3 )
			{
				// Merge them
				curr.nodeValue += curr.nextSibling.nodeValue;
				curr.parentNode.removeChild( curr.nextSibling );
				i--;
			}
		}
		
		// Recursive step for element nodes
		if( curr.firstChild )
			mergeTextNodes( curr );
	}
}





// iconURLS will store the paths to important icons
var iconURLs = new Object();
iconURLs.sortedAscending = 'http://www.commercial.carrier.com/Images/Commercial_Systems/Global/US-en/icons/sortable-ascending.gif';
iconURLs.sortedDescending = 'http://www.commercial.carrier.com/Images/Commercial_Systems/Global/US-en/icons/sortable-descending.gif';
iconURLs.unsorted = 'http://www.commercial.carrier.com/Images/Commercial_Systems/Global/US-en/icons/sortable-unsorted.gif';





function preloadImgs()
{
	for( var icon in iconURLs )
	{
		var temp = new Image( 1, 1 );
		temp.src = iconURLs[icon];
	}
}



















































