

	// new SortingTable('my_table', {
	//   zebra: true,     // Stripe the table, also on initialize
	//   details: false   // Has details every other row
	// });
	//
	// The above were the defaults.  The regexes in loadConversions test a cell
	// begin sorted for a match, then use that conversion for all elements on that
	// column.
	//
	// Requires mootools Class, Array, Function, Element, Element.Selectors,
	// Element.Event, and you should probably get Window.DomReady if you're smart.
	//
	
	// v2.5 Modified by Studio EFX on 4/3/08, major code refactoring, updated comparison methods.
	// v2.6 Modified by Studio EFX on 9/10/09, updated for mootools >= 1.2.3 compatibility.
	
	var SortingTable = new Class({

		Implements: Options,

		options: {
			zebra: true,
			details: false
		},

		initialize: function(table, options) {
			var self = this;

			this.removeAltClassRegExp = new RegExp('(^|\\s)alt(?:\\s|$)'),

			this.setOptions(options);
			this.headers = new Hash();
			this.table = $(table);
			this.tbody = this.table.getElement('tbody');
			this.thead = this.table.getElement('thead');
			this.sortColumn = null; // @integer

			// add zebra stripes if requested in options
			//if (this.options.zebra) SortingTable.stripeTable($ES('tr',this.tbody));
			if (this.options.zebra) this.stripeTable();

			// create header click events
			this.createHeaderClickEvents();

			// create array of all conversion methods
			this.loadConversions();
		},
	
		sortByHeader: function(headerText){
			
			var self = this;
	
			// clear all direction arrows from header cells, prepare the array
			// that memorizes the header arrows and set the next toggle state
			// for any header that displays a directional arrow.
			var arrows = this.clearHeaderArrowsAndUpdateState(arrows);
			
			// remove all rows from DOM, but store them first for later processing
			this.rows = this.removeAndReplaceRows();

			// fetch value from header hash
			var header = this.headers.get(headerText);

			if(this.sortColumn >= 0 && this.sortColumn == header.column)
			{
				// update arrow pointer
				$(header.cell).addClass(arrows.nextClass);

				// if the same header is clicked twice in succession, the column does not need to be resorted.
				// Skip sort, and simply invert table order.
			}
			else
			{
				// update arrow pointer
				$(header.cell).addClass('up');

				// update current sort column
				this.sortColumn = header.column;

				if (header.conversionFunction) {
					this.conversionFunction = header.conversionFunction;
					this.sortFunction = header.sortFunction;
				} else {
					this.conversionFunction = false;
					this.sortFunction = null;

					// determine which conversion method to apply to column
					this.rows.some(function(row) {
						// this fetches all td cells from the current row, then picks out the Nth td cell specified by sortColumn
						var el = $(row.row).getElements('td')[self.sortColumn];
						var toMatch = self.cleanString($(el).get('text'));
						// break on empty cell
						if(toMatch == '') return false;

						// loop until a conversion function matches the cell data type, then assign it
						self.conversions.some(function(conversion) {
							if(!conversion.pattern.test(toMatch)) return false;
							self.conversionFunction = conversion.conversionFunction;
							self.sortFunction = conversion.sortFunction;
							return true;
						});

						// update conversion function hash
						header.conversionFunction = self.conversionFunction.bind(self);
						header.sortFunction	= self.sortFunction; // nothing to bind to
						self.headers.set(headerText, header);
						return true;
					});
				}
				if(this.conversionFunction !== false)
				{
					// convert cell data to sortable data types
					this.rows.each(function(row, key) {
						row.compareValue = self.conversionFunction(row);
						row.toString = function(){ return this.compareValue};
					});
					// sort array, use custom sort function if assigned in conversion method setup
					if(this.sortFunction)
					{
						this.rows.sort(this.sortFunction);
					}
					else
					{
						this.rows.sort();
					}
				}
			}
	
			// rebuild rows in new sorted order
			this.rebuildRowsFromSortedRows();

			this.rows = false;
		},
	
		loadConversions: function() {
			var self = this;
			this.conversions = [
			{
			/*	Date Format 1:
				m/d/yy
				m-d-yy
				mm/dd/yy
				mm-dd-yy */
				pattern: /^\d{1,2}[-\/]\d{1,2}[-\/]\d{2}/i,
				sortFunction:  function(a, b)
				{
					// sort as integers
					return self.cleanNumber(a) - self.cleanNumber(b);
				},
				conversionFunction: function(row) {
					// var cell = $ES('td', row.row)[this.sortColumn].getText();
					var el = $(row.row).getElements('td')[this.sortColumn];
					var value = self.cleanString($(el).get('text'));
					var pattern = /(\d{1,2})[-\/](\d{1,2})[-\/](\d{2})/i;
					value = pattern.exec(value);
					if(value)
					{
						// (parseInt(cell[3]) < 0) ? cell[3] = '19' + cell[3] : cell[3] = '20' + cell[3];
						value[3] += (parseInt(value[3]) < 0) ? '19' : '20';
						return Date.UTC(value[3], value[1], value[2]);
					}
					return 0;
				}
			},{
				// Integer and Float
				pattern: /^\d+[\d\.\(\)\-\%]*$/i,
				sortFunction: function(a, b)
				{
					// sort as integers
					return self.cleanNumber(a) - self.cleanNumber(b);
				},
				conversionFunction: function(row) {
					var el = $(row.row).getElements('td')[this.sortColumn];
					var value = self.cleanString($(el).get('text'));
					value = value.replace(/[^\d\.\-]/ig, '');
					return value;
				}
			},{
			/*	Currency:
				$1.05
				$43,311,50
				$0.35			*/
				pattern: /^\$\d+/i,
				sortFunction:  function(a, b)
				{
					// sort as floats
					return self.cleanNumber(a) - self.cleanNumber(b);
				},
				conversionFunction: function(row)
				{
					var el = $(row.row).getElements('td')[this.sortColumn];
					var value = self.cleanString($(el).get('text'));
					value = value.replace(/[^\d\.]/ig, '');
					return value;
				}
			},{
				// Default 
				pattern: /.*/i,
				sortFunction: null,
				conversionFunction: function(row)
				{
					var el = $(row.row).getElements('td')[this.sortColumn];
					var value = self.cleanString($(el).get('text'));
					value = value.toLowerCase();
					return value;
				}
			}
			];
			//}]);
		},

		stripeTable : function()
		{
			var self = this;
			var rows = $(this.tbody).getElements('tr');
			var counter = 0;
			$each(rows, function(el){
				if(el.getStyle('display') != 'none' && !el.hasClass('collapsed'))
				{
					counter++;
				}
				el.className = self.cleanString(el.className.replace(this.removeAltClassRegExp, '$1'));
				if(!((counter % 2) == 0))
				{
					el.addClass('alt');
				}
			});
		},
		

		
		/*
		**
		** PRIVATE METHODS
		**
		*/

		cleanString: function(str)
		{
			var value = str.replace(/\&nbsp\;/i, ' ');
			value = value.replace(/\&amp\;/i, ' ');
			return value.clean();
		},

		cleanNumber: function(o)
		{
			var value = o.compareValue;
			// make sure only one decimal digit
			var n = false;
			value = value.toString().replace(/\./g, function(s) {
				if(n) return '';
				n = true;
				return '.';
			});
			value = value.replace(/[^0-9\.\-]/ig, '');
			value = value.toFloat();
			if(isNaN(value)) value = 0;
			return value;
		},

		createHeaderClickEvents: function()
		{
			var self = this;
			$(this.thead).getElements('th').each(function(val, key) {
				var header = $(val);
				// make sure header cell allows sort
				if(!header.hasClass('no_sort'))
				{
					//this.headers.set(header.getText(),{
					self.headers.set(header.get('text').clean(), {
						cell: header,
						column: key
					});
					header.addEvent('click', function(e) {
						self.sortByHeader($(e.target).get('text').clean());
					});
				}
			});
		},


		clearHeaderArrowsAndUpdateState : function(arrows)
		{
			var arrows = {
				currKey: null,
				nextClass: ''
			};
			// locate the header that has a direction arrow class, prepare the toggle state,
			// then clear the arrow. All header arrows should be cleared by the end of this method.
			this.headers.each(function(value, key)
			{
				var c = $(value.cell);
				if(c.hasClass('up'))
				{
					arrows.currKey = key;
					arrows.nextClass = 'down';
					c.removeClass('up');
				}
				else if(c.hasClass('down'))
				{
					arrows.currKey = key;
					arrows.nextClass = 'up';
					c.removeClass('down');
				}
			});
			return arrows;
		},
		
		
		removeAndReplaceRows: function()
		{
			var rows = new Array();
			// build new array of rows while removing rows from current table
			$(this.tbody).getElements('tr').each(function(val, key) {
				var row = {row: $(val).dispose()}
				rows.unshift(row);
			});
			return rows;
		},
		
		
		// rebuild table in new sorted order
		rebuildRowsFromSortedRows: function()
		{
			var i = 0;
			while(row = this.rows.shift())
			{
				$(row.row).injectInside(this.tbody);
				if(row.detail)
				{
					$(row.detail).injectInside(this.tbody)
				}
				// add zebra stripes
				if(this.options.zebra)
				{
					$(row.row).className = $(row.row).className.replace(this.removeAltClassRe, '$1').clean();
					if(row.detail)
					{
						$(row.detail).className = $(row.detail).className.replace(this.removeAltClassRe, '$1').clean();
					}
					if((i++ % 2) == 0)
					{
						$(row.row).addClass('alt');
						if(row.detail)
						{
							$(row.detail).addClass('alt');
						}
					}
				}
			}
		}
	
	});
