/*
  (c) 2004-2005 P/PM Services by Douglas Clayton.  

*/

/*
COOKIES

In addition to setting cookies to remember the current state of the list, this code sets
three other cookies intended to communicate back to the server.

navigation: Contains the row ids for the first, previous, next, last rows
separated by &.  If there is more than one row selected, first/previous/next/last
refer to the selected subset. If there is one row selected, first/last refer to the first
and last in the filtered list, and previous/next refer to the selected row.  If there are
no rows selected, first/last refer to the first and last in the filtered list, previous is empty,
and next refers to the last row (or empty if there is only one row in the filtered list).

selection: Encodes the rows that are selected.  An array of characters, one for each row
in the UN-filtered list, in the order they are currently displayed (see selection-sort).
If the character at position n is 1, then row n is selected; if it is 0, then it is not selected.
The filtering is encoded into the selection list so that the server does not need to reconstruct the
filter in use.

selection-sort: Encodes the sort currently used.  If there is only one sorted column, it is encoded
as [0-9]+ followed by either - for descending or + for ascending.  The column number is zero-based
and refers to the column order in the data.

PERFORMANCE

IE's garbage collector's running time is proportional to the data loaded, because each time it 
runs it has to analyze all of the working set to find unreferenced data ("garbage").  So we can't simply
load up all the data and then render a portion of it.  Instead, we use a clever hack: the data
is all defined as static arrays and hash tables inside a function, like this:

function dataForSomeColumn()
{
	// has one entry for each row in the data
	return { "first row", "second row", "third row", .... "last row" };
}

We then call this function *each time* we need to get at the data.  Because the array is hard-coded,
it loads extremely quickly, but because we do not hang onto a reference to it after we are done
extracting our data, it gets garbage-collected and does not continue to be a drain on performance.

Furthermore, there is a separate function for each column of data (two, actually: one for the displayed values,
and one for the sort values).  There is also one function for row-associated data, like the IDs and query strings.
This lets us load up just the columns we need, which means at no time do we have the entire data loaded in memory,
and only for brief periods do we have the entire data for one column.

The dataSource parameter to DataSet() is an object, ultimately, of functions (it consists of arrays and objects that 
hold other arrays and objects which ultimately hold functions like the one above).
*/

var spaceCharacter = " ";

var imageConversionRE = /\.png/g;

/************************* DataSet ****************************************/

/* Reads all the data in the given array into the Entries
   array, then filters the data to make the Lines array. 
   
*/

function DataSet(totalEntries, data, sorted_column, ascending, columnDefinitions, currentRevision, oldRowIdRevision)
{
//	startTimer("load", "make data");

	this.dataSource = data;
	
	this.totalEntries = this.filteredRowCount = totalEntries;

	// the index at n is the source row at the nth position, based on the current order.
	// this only changes when the sort order changes.
	this.UnfilteredViewToSourceRow = new Array();
	// the index at n is the view row for the nth source row, based on the current order
	// this only changes when the sort order changes.
	this.UnfilteredSourceToViewRow = new Array();
		
	// the index at n is the source row at the nth position, based on the current order and filter.
	// this should be recreated this.UnfilteredViewToSourceRow when the filter changes.
	// This is only as long as there are items in view.
	this.FilteredViewToSourceRow = new Array();
	
	// the index at n is the view row for the nth source row, based on the current order and filter.
	// this should be recreated this.UnfilteredSourceToViewRow when the filter changes.
	// This is the same length as this.UnfilteredSourceToViewRow, but source items
	// which are not current in the filter are null.
	this.FilteredSourceToViewRow = new Array();
		
	this.IncludedRows = new Array();
		
	// set up an identity mapping (default sort order and no filter)
	for (var i = 0; i < this.totalEntries; i++)
	{
		this.UnfilteredViewToSourceRow[i] = i;
		this.UnfilteredSourceToViewRow[i] = i;
		this.FilteredViewToSourceRow[i] = i;
		this.FilteredSourceToViewRow[i] = i;
		this.IncludedRows[i] = true;
	}

	this.columnDefinitions = columnDefinitions;
	this.columnCount = data.columns.length;

	// a reverse mapping from column name to source column index
	this.columnsByName = {};
	
	this.editData = null;
	
	this.dataSource.columnsByName = {};
	for (var i = 0; i < this.columnCount; i++)
	{
		var name = columnDefinitions[i].name;
		this.columnsByName[name] = i;
		// also duplicate the data-source function for this column
		this.dataSource.columnsByName[name] = this.dataSource.columns[i];
	}
	
	this.oldRowIdRevision = oldRowIdRevision;
	this.currentRevision = currentRevision;	
	
	// first set the sort order
	this.sorted_column = sorted_column;
	this.ascending = ascending;

	// make the initial filter (which doesn't exclude anything)
	this.include = new MultiFilter();
	
	// true if the row at the source row is selected or false if not
	this.selectedRows = new Array();

	this.currentRow = null;
	
	this.referenceRow = null;
	
	this.deletedRowLookup = new Object();
	
//	stopTimer("load");
}


DataSet.prototype.MakeEditable = function()
{
	this.editData = new Array();
	for (var col = 0; col < this.columnCount; col++)
	{
		this.editData[col] = new Array();
		var data = this.dataSource.columns[col].text();
		for (var sourceRow = 0; sourceRow < this.totalEntries; sourceRow++)
		{
			this.editData[col][sourceRow] = data[sourceRow];
		}
	}
	
	this.editIDs = new Array();
	var ids = this.dataSource.rowIDs();
	for (var sourceRow = 0; sourceRow < this.totalEntries; sourceRow++)
	{
		this.editIDs[sourceRow] = ids[sourceRow];
	}
}

DataSet.prototype.RemoveLastEditRow = function()
{
	this.UnfilteredViewToSourceRow.length--;
	this.UnfilteredSourceToViewRow.length--;
	this.IncludedRows.length--;
	
	this.editIDs.length--;
	for (var col = 0; col < this.columnCount; col++)
	{
		this.editData[col].length--;
	}		
	
	this.totalEntries--;
	this._RefreshFilteredLines();
};

DataSet.prototype.AddEditRow = function(values, id)
{
	var newSourceRow = this.totalEntries;
	
	for (var col = 0; col < this.columnCount; col++)
	{
		this.editData[col].push(values[col]);
	}		
	
	this.editIDs.push(id);
	
	this.UnfilteredViewToSourceRow.push(newSourceRow);
	this.UnfilteredSourceToViewRow.push(newSourceRow);
	this.IncludedRows.push(true);
	this.totalEntries++;
	
	this._RefreshFilteredLines();
	
	return newSourceRow;
};

DataSet.prototype.SetEditValue = function(viewRow, colNumber, value)
{
	this.editData[colNumber][this.FilteredViewToSourceRow[viewRow]] = value;
}

DataSet.prototype.GetEditValue = function(viewRow, colNumber)
{
	return this.editData[colNumber][this.FilteredViewToSourceRow[viewRow]];
}

DataSet.prototype._GetColumnText = function(col)
{
	if (this.editData != null)
	{
		return this.editData[col];
	}
	else
	{
		return this.dataSource.columns[col].text();
	}
}

DataSet.prototype._GetColumnSortData = function(col)
{
	if (this.editData != null)
	{
		return this.editData[col];
	}
	else
	{
		return this.dataSource.columns[col].sort_data();
	}
}

DataSet.prototype._GetIDs = function()
{
	if (this.editIDs != null)	
	{
		return this.editIDs;
	}
	else
	{
		return this.dataSource.rowIDs();
	}
}

DataSet.prototype.GetViewData = function(startRow, rowCount)
{
	startTimer("getviewdata", "GetViewData");
	
	var viewData = { 'cells' : [], 'ids' : [], 'parameters' : [] };
	
	var endRow = Math.min(startRow + rowCount - 1, this.filteredRowCount - 1);
	var maxRows = endRow - startRow + 1;
	
	// first initialize each row of cells
	for (var n = 0; n < maxRows; n++)
	{
		viewData.cells[n] = new Array();
	}	
	
	var ids = this._GetIDs();
	
	// load in the IDs
	var viewRow = startRow;
	for (n = 0; n < maxRows; n++, viewRow++)
	{
		viewData.ids[n] = ids[this.FilteredViewToSourceRow[viewRow]];
	}

	// done with the data, so we make it available for GC immediately
	ids = null;
		
	var parameters = this.dataSource.rowParameters();
	
	// load in the parameters
	var viewRow = startRow;
	for (n = 0; n < maxRows; n++, viewRow++)
	{
		viewData.parameters[n] = parameters[this.FilteredViewToSourceRow[viewRow]];
	}

	// done with the data, so we make it available for GC immediately
	parameters = null;
		
	var ellipsis = "...";
	
	for (var col = 0; col < this.columnCount; col++)
	{
		var maxDisplayLength = this.columnDefinitions[col].max_display_length;
		var enumeratedTable = this.columnDefinitions[col].enumerated;
		var data = this._GetColumnText(col);
		
		var viewRow = startRow;
		for (n = 0; n < maxRows; n++, viewRow++)
		{
			var cell = new Object();
			viewData.cells[n][col] = cell;
			
			var sourceRow = this.FilteredViewToSourceRow[viewRow];
			var val = data[sourceRow];
			cell.text = val;

			var display = val;
			var title = "";

			if (display === null || display.length == 0)
			{
				// null data or empty strings get a nonbreaking space
				display = nonbreakingSpace;
			}
			else if (enumeratedTable)
			{
				var enumerated = enumeratedTable[val];
				if (enumerated && enumerated.description)
				{
					title = enumerated.description;
				}
			}
			else if (maxDisplayLength && display.length > maxDisplayLength)
			{
	            // add the full value to the title
	            title = display;
	            
	            // but show a shortened value
	            display = val.slice(0, maxDisplayLength - 3) + ellipsis;
			}
			
			cell.display = display;
			cell.title = title;
		}	
		
		// done with the data for this column
		data = null;
	}	
	
	stopTimer("getviewdata");
	return viewData;
}

DataSet.prototype.FindViewRowForID = function(id)
{
	var ids = this._GetIDs();
	
	for (var viewRow = 0; viewRow < this.filteredRowCount; viewRow++)
	{
		if (ids[this.FilteredViewToSourceRow[viewRow]] == id)
		{
			return viewRow;
		}
	}
	
	return null;
}


DataSet.prototype.SetFilter = function (key, filter)
{
	this.include.SetFilter(key, filter);
	this._FilterLines();
};

DataSet.prototype.RemoveFilter = function (key)
{
	this.include.RemoveFilter(key);
	this._FilterLines();
};

DataSet.prototype.HasFilter = function (key)
{
	return this.include.HasFilter(key);
};

DataSet.prototype.GetFilter = function (key)
{
	return this.include.GetFilter(key);
}

DataSet.prototype.GetFilterState = function()
{
	return this.include.GetState();
};


DataSet.prototype.LoadFilter = function (state)
{
	// we always use a multifilter
	this.include = MultiFilter.Load(state, this);
	this._FilterLines();
};

/* Defines sort order.  After this function, the dataset will always remain sorted in accordance 
with the settings given (even when filters are added/removed).
sorted_column: the column (zero-based) to sort by
ascending: true if the column should be sorted ascending, false if descending
*/
DataSet.prototype.SetSortOrder = function (sorted_column, ascending)
{
	// first remember the settings
	this.sorted_column = sorted_column;
	this.ascending = ascending;
	
	// then sort the lines
	this._SortLines();
}

DataSet.prototype.IsAscending = function()
{
	return this.ascending;
}

DataSet.prototype.GetSortedColumn = function()
{
	return this.sorted_column;
}


// returns true if the given row is selected
DataSet.prototype.IsViewRowSelected = function(viewRow)
{
	return this.selectedRows[this.FilteredViewToSourceRow[viewRow]];
};


DataSet.prototype.IsViewRowReference = function(viewRow)
{
	return this.referenceRow == this.FilteredViewToSourceRow[viewRow];
};

DataSet.prototype.SelectExclusive = function(absoluteRowNumber)
{
	// remove all selections then select this row
	this.ClearSelections();
	this.SelectAdditive(absoluteRowNumber);

	this.CachedSelectList = null;
}

DataSet.prototype.SelectAll = function()
{
	// first remove all selections 
	this.ClearSelections();

	for (var viewRow = 0; viewRow < this.filteredRowCount; viewRow++)
	{
		var sourceRow = this.FilteredViewToSourceRow[viewRow];
		this.selectedRows[sourceRow] = true;
	}

	this.CachedSelectList = null;
}

DataSet.prototype.SelectGiven = function(viewRows)
{
	// first remove all selections 
	this.ClearSelections();

	for (var n = 0; n < viewRows.length; n++)
	{
		this.SelectAdditive(viewRows[n]);
	}

	this.CachedSelectList = null;
}

DataSet.prototype.SelectAdditive = function(viewRow)
{
	var sourceRow = this.FilteredViewToSourceRow[viewRow];
	this.selectedRows[sourceRow] = true;
	this.CachedSelectList = null;
}

DataSet.prototype.SelectIntermediate = function(givenViewRow, exclusive, updateCurrent)
{
	// remember this before we clear it
	var existingCurrentRow = this.currentRow;
		
	if (updateCurrent)
	{
		// remove this first, then restore it or not based on whether it is selected
		this.RemoveCurrent();
	}
	
	if (exclusive)
	{
		this.ClearSelections();
	}

	var referenceRow = this.GetReferenceRow();
	if (referenceRow == null)
	{
		referenceRow = 0;
	}

	// pick the starting and ending rows (both view based, rather than source based)
	var referenceViewRow = this.FilteredSourceToViewRow[referenceRow];
	var startRow = referenceViewRow;
	var endRow = givenViewRow;
	if (givenViewRow < referenceViewRow)
	{
		startRow = givenViewRow;
		endRow = referenceViewRow;
	}
	
	for (var viewRow = startRow; viewRow <= endRow; viewRow++)
	{
		var sourceRow = this.FilteredViewToSourceRow[viewRow];
		this.selectedRows[sourceRow] = true;
	}
		
	this.CachedSelectList = null;
		
	// if this is not exclusive, then the reference point moves with the 
	// mouse; if it is, then it stays where it is as an anchor
	if (!exclusive)
	{
		this.MakeReference(givenViewRow);
	}	
	
	// if there was an existing current row and it falls in 
	// the range of now-selected samples, then restore it
	if (existingCurrentRow != null)
	{
		var currentViewRow = this.FilteredSourceToViewRow[existingCurrentRow];
		if (currentViewRow >= startRow && currentViewRow <= endRow && updateCurrent)
		{
			this.MakeViewRowCurrent(currentViewRow);
		}
	}
}

DataSet.prototype.ClearSelections = function()
{
	this.selectedRows = new Array();
	this.CachedSelectList = null;
};

DataSet.prototype.UnselectRow = function(viewRow)
{
	var sourceRow = this.FilteredViewToSourceRow[viewRow];
	this.selectedRows[sourceRow] = false;
	this.CachedSelectList = null;
};


DataSet.prototype.MakeViewRowCurrent = function(viewRow)
{
	this.currentRow = this.FilteredViewToSourceRow[viewRow];
}

DataSet.prototype.MakeSourceRowCurrent = function(sourceRow)
{
	this.currentRow = sourceRow;
}

DataSet.prototype.RemoveCurrent = function()
{
	this.currentRow = null;
}

// returns the current row, or null if none
DataSet.prototype.GetCurrentSourceRow = function()
{
	return this.currentRow;
};

// returns true if the given row is the current row
DataSet.prototype.IsViewRowCurrent = function(viewRow)
{
	return this.currentRow == this.FilteredViewToSourceRow[viewRow];
};


// returns the reference row, or null if none
DataSet.prototype.GetReferenceRow = function()
{
	return this.referenceRow;
};

DataSet.prototype.MakeReference = function(viewRow)
{
	this.referenceRow = this.FilteredViewToSourceRow[viewRow];
}

DataSet.prototype.RemoveReference = function()
{
	this.referenceRow = null;
}

// Deletes/undeletes the given rows (depending on the boolean 'deleted')
DataSet.prototype.SetDeletedStatus = function(viewRowNumbers, deleted)
{
	for (var n = 0; n < viewRowNumbers.length; n++)
	{
		var sourceRow = this.FilteredViewToSourceRow[viewRowNumbers[n]];
		if (deleted)
		{
			this.deletedRowLookup[sourceRow] = true;
		}
		else
		{
			delete this.deletedRowLookup[sourceRow];
		}
	}
}


DataSet.prototype.IsViewRowDeleted = function(viewRow)
{
	return this.deletedRowLookup[this.FilteredViewToSourceRow[viewRow]] === true;
};


DataSet.prototype.GetSelectionCount = function()
{
	var selectedRows = this.GetSelectedViewRows();
	return selectedRows.length;
};



// returns a binary string in the original source order,
// which includes selections ONLY (filters are not taken into account)
DataSet.prototype.GetSelectionState = function()
{
	var selected = new Array();

	for (var sourceRow = 0; sourceRow < this.selectedRows.length; sourceRow++)
	{
		if (this.selectedRows[sourceRow])
		{
			selected[sourceRow] = '1';
		}
		else
		{
			selected[sourceRow] = '0';
		}				
	}

	return selected.join("");
}


/* returns an array of source row indexes for the rows that are selected,
in the order they are visible */
DataSet.prototype.GetSelectedViewRows = function()
{
	if (this.CachedSelectList == null)
	{
		this.CachedSelectList = new Array();
		for (var viewRow = 0; viewRow < this.filteredRowCount; viewRow++)
		{
			var sourceRow = this.FilteredViewToSourceRow[viewRow];
			if (this.selectedRows[sourceRow])
			{
				this.CachedSelectList[this.CachedSelectList.length] = viewRow;
			}
		}
	}
	
	return this.CachedSelectList;
};

DataSet.prototype.GetVisibleSelection = function(invalidNavigationIndicator)
{
	// start with the current row, unless it is not visible
	var currentViewRow = this.FilteredSourceToViewRow[this.currentRow];
	
	var currentRowOffset = 0;
	
	// an array with an element for every row in the original source,
	// in the order of the current filter. Each element is either 0 or 1.
	var selectionState = new Array();
	
	// a list of all the selected view rows, in the order they occur
	var selectedViewRows = new Array();
	
	for (var unfilteredViewRow = 0; unfilteredViewRow < this.totalEntries; unfilteredViewRow++)
	{
		var sourceRow = this.UnfilteredViewToSourceRow[unfilteredViewRow];
		// this might be null if this row is not visible
		var filteredViewRow = this.FilteredSourceToViewRow[sourceRow];
		// we only consider this row if selected AND filtered in
		if (this.selectedRows[sourceRow] && filteredViewRow !== null)
		{
			selectionState[unfilteredViewRow] = '1';
			selectedViewRows.push(filteredViewRow);
			
			// if we do not have a current row yet, take this one, 
			// which will be the first selected row
			if (currentViewRow == null)
			{
				currentViewRow = filteredViewRow;
			}
			else if (currentViewRow == filteredViewRow)
			{	
				// we found the current row *after* adding n items, so the offset is the position of the last one
				currentRowOffset = selectedViewRows.length-1;
			}
		}
		else
		{
			selectionState[unfilteredViewRow] = '0';
		}		
	}

	if (currentViewRow == null)
	{
		// there was no current row and no selection, so take the first one in the list
		currentViewRow = 0;
	}	
	
	var navigationRows = new Array();
	if (selectedViewRows.length <= 1)	
	{	
		// use the entire list 
		navigationRows.push(0);
		navigationRows.push(currentViewRow-1);
		navigationRows.push(currentViewRow+1);
		navigationRows.push(this.filteredRowCount-1);
	}
	else 
	{
		navigationRows.push(selectedViewRows[0]);
		navigationRows.push(selectedViewRows[Math.max(currentRowOffset-1, 0)]);
		navigationRows.push(selectedViewRows[Math.min(currentRowOffset+1, selectedViewRows.length-1)]);
		navigationRows.push(selectedViewRows[selectedViewRows.length-1]);
	}

	var ids = this.dataSource.rowIDs();
	var navigationIDs = new Array();
	for (var n = 0; n < navigationRows.length; n++)
	{
		var viewRow = navigationRows[n];
		if (viewRow >= 0 && viewRow < this.filteredRowCount && viewRow != currentViewRow)
		{
			// convert this view row to a source row, then get the ID for that
			var sourceRow = this.FilteredViewToSourceRow[viewRow];
			navigationIDs.push(ids[sourceRow]);
		}
		else
		{
			navigationIDs.push(invalidNavigationIndicator);
		}
	}

	return { "selection" : selectionState.join(""), "currentViewRow" : currentViewRow, 
	"currentRowOffset" : currentRowOffset, "firstSelectedViewRow" : selectedViewRows[0],
	"navigationIDs" : navigationIDs, "navigationViewRows" : navigationRows };
}




DataSet.prototype.GetViewRowParameter = function(viewRow)
{
	var parameters = this.dataSource.rowParameters();
	return parameters[this.FilteredViewToSourceRow[viewRow]];
}



DataSet.prototype.RestoreSelections = function(selections, currentRow, rev)
{
	// we can only restore selections if we have the identifiers as
	// they were when we encoded the selected positions,
	// and if the identifiers come from the same revision as
	// our selections	
	if (this.oldRowIdRevision == rev)
	{
		var oldIDs = this.dataSource.oldIDs();
		var newIDs = this.dataSource.rowIDs();
		var newIDMapping = new Object();
		for (var n = 0; n < newIDs.length; n++)
		{
			// map the ID at this row to the row number
			newIDMapping[newIDs[n]] = n;
		}

		// only restore if we were given the old IDs--otherwise we
		// don't know what the positions refer to
		if (oldIDs != null)
		{
			// convert the selections
			for (var i = 0; i < selections.length; i++)
			{
				// get the ID of the old row...
				var id = oldIDs[i];
				// ...and use that to find the new rows
				this.selectedRows[newIDMapping[id]] = (selections.charAt(i) == '1');
			}
			
			// convert the curret row, if there is one
			if (currentRow != null)
			{
				// get the ID of the old row...
				var id = oldIDs[currentRow];
				// ...and use that to find the new row
				this.currentRow = newIDMapping[id];
			}
		}
	}	
	else if (this.currentRevision == rev)
	{
		for (var i = 0; i < selections.length; i++)
		{
			this.selectedRows[i] = (selections.charAt(i) == '1');
		}
		
		this.currentRow = currentRow;
	}
}


DataSet.prototype.FilteredSize = function()
{
	return this.filteredRowCount;
}

DataSet.prototype.TotalSize = function()
{
	return this.totalEntries;
}

DataSet.prototype.isFilterActive = function()
{
	return this.include.isFilterActive();
}

DataSet.prototype.MakeSetFromColumn = function (col, displayedSet, valueSet)
{
	var fullData = this.dataSource.columns[col].text();
	
	var maxLength = this.columnDefinitions[col].max_display_length;

	var added = new Object();
	var ellipsis = "...";
	for (var viewRow = 0; viewRow < this.filteredRowCount; viewRow++)
	{
		// get the source row for this line and look up the data
		var sourceRow = this.FilteredViewToSourceRow[viewRow];
		var text = fullData[sourceRow];
		if (!added[text])
		{
			// the text needs no adjustment
			valueSet[valueSet.length] = text;
			if (maxLength && text.length > maxLength)
			{
				displayedSet[displayedSet.length] = text.slice(0, maxLength-3) + ellipsis;
			}
			else
			{
				displayedSet[displayedSet.length] = text;
			}

			// now remember that we added this already
			added[text] = true;
		}	
	}
	
	fullData = null;
	
	// this may not work if the values differ from the text more than a simple truncation
	displayedSet.sort();
	valueSet.sort();
}

/* Fills in the RowIndexes array with the data that is included by the current filter. */
DataSet.prototype._FilterLines = function()
{
	startTimer("filter", "filter");
	
	this.IncludedRows = this.include.Filter(this.dataSource);

	this._RefreshFilteredLines();
	
	stopTimer("filter");
}


/* A filter that includes any row which contains the entire string in the given column's text.  */
function MatchFilter(columnName, m)
{
	this.columnName = columnName;
	this.m = m;
}

MatchFilter.prototype.Filter = function(dataSource)
{
	var data = dataSource.columnsByName[this.columnName].text();
	
	var filtered = new Array();
		
	for (var row = 0, rowLen = data.length; row < rowLen; row++)
	{	
		filtered[filtered.length] = (data[row] == this.m);
	}
	
	return filtered;
}


// returns encoded state of this filter
MatchFilter.prototype.GetState = function()
{
	return [ this.columnName, this.m ];
}

MatchFilter.prototype.GetMatchText = function()
{
	return this.m;
}


MatchFilter.Load = function(state, dataset)
{
	var columnName = state[0];
	if (!isNaN(parseInt(columnName)))
	{
		// For backwards compatibility, we have to handle this being a number
		columnName = dataset.columnDefinitions[state[0]].name;
	}

	if (isNumber(dataset.columnsByName[columnName]))
	{
		return new MatchFilter(columnName, state[1]);
	}
	else
	{
		return null;
	}
}

MatchFilter.prototype.GetClassName = function()
{
	return "MatchFilter";
}


/* A filter that includes any row which contains the entire string in the any column's text. */
function RowMatchFilter(matchText)
{
	this.matchText = matchText;
	this.matchParts = matchText.toLowerCase().split(" ");
}

RowMatchFilter.prototype.Filter = function(dataSource)
{
	var data = dataSource.entireRows();
	var filtered = new Array();
		
	for (var row = 0, rowLen = data.length; row < rowLen; row++)
	{	
		var rowText = data[row];
		var included = true;
		for (var i = 0, len = this.matchParts.length; i < len; i++)
		{
			if (rowText.indexOf(this.matchParts[i]) == -1)
			{
				// this part isn't here, so the row is not a match
				included = false;
				break;
			}
		}
		
		filtered[filtered.length] = included;
	}
	
	return filtered;
}

RowMatchFilter.prototype.GetState = function()
{
	return [ this.matchText ];
}

RowMatchFilter.prototype.GetMatchText = function()
{
	return this.matchText;
}


RowMatchFilter.Load = function(state, dataset)
{
	return new RowMatchFilter(state[0]);
}

RowMatchFilter.prototype.GetClassName = function()
{
	return "RowMatchFilter";
}

/* A filter that includes any row which is included by a list of subfilters. 
This list of subfilters can be added to and removed from. */
function MultiFilter()
{
	// initially there are no subfilters
	this.filters = new Object();
}

// sets the subfilter for the given key. If there is currently no
// subfilter for that key, one will be added; if there is, that subfilter
// will be overwritten.
MultiFilter.prototype.SetFilter = function (key, filter)
{
	// this either adds or overwrites the filter
	this.filters[key] = filter;
}

MultiFilter.prototype.RemoveFilter = function (key)
{
	delete this.filters[key];
}

MultiFilter.prototype.HasFilter = function (key)
{
	return typeof(this.filters[key]) != "undefined";
}

MultiFilter.prototype.GetFilter = function (key)
{
	return this.filters[key];
}

MultiFilter.prototype.isFilterActive = function ()
{
	var count = 0;
	for (var f in this.filters)
	{
		count++;
	}
	return count > 0;
}


MultiFilter.prototype.Filter = function(dataSource)
{
	var filtered = new Array();
	for (key in this.filters)
	{
		var filter = this.filters[key];
		var result = filter.Filter(dataSource);
		if (result != null && result.length > 0)
		{
			filtered[filtered.length] = result;
		}
	}

	var filterCount = filtered.length;
	
	if (filterCount == 0)
	{
		// no filters in effect, so we return a special value
		return null;
	}
	else if (filterCount == 1)
	{
		// just the one filter, so we don't need to do a merge
		return filtered[0];
	}
	else
	{
		// merge the N filtered arrays into one
		var result = new Array();
		var rowCount = filtered[0].length;
		for (var row = 0; row < rowCount; row++)
		{
			var included = true;
			for (var i = 0; i < filterCount; i++)
			{
				if (!filtered[i][row]) 
				{
					included = false;
					break;
				}
			}		
			
			result[row] = included;	
		}
		
		return result;
	}
}

MultiFilter.prototype.GetState = function()
{
	var state = new Object();

	for (key in this.filters)
	{	
		var filter = this.filters[key];
		var s = [ filter.GetClassName(), key, filter.GetState() ];
		state[key] = s;
	}	
	
	return state;
}

// old number-based keys took this form
var backwardsCompatibleColumnFixRE = /col(\d+)/;

MultiFilter.Load = function(state, dataset)
{
	var newMultiFilter = new MultiFilter();
	
	for (key in state)
	{
		// break this filter down into its components
		var results = state[key];
				
		var className = results[0];
		if (className)
		{
			var key = results[1];
			var columnMatch = backwardsCompatibleColumnFixRE.exec(key);
			if (columnMatch)
			{
				// For backwards compatibility, we have to convert the column-number-based key
				// to a name-based one (including an extra dash)
				key = "col-" + dataset.columnDefinitions[columnMatch[1]].name;
			}
			
			// generate the load code by compiling the appropriate expression
			var filterState = results[2];
			var expr = "return " + className + ".Load";
	
			// run this code to get the new filter object...
			var filterFunc = new Function(expr)();
			var filter = filterFunc(filterState, dataset);
	
			if (filter)
			{
				// ...and add this filter object with the right key
				newMultiFilter.SetFilter(key, filter);
			}
		}
	}
	
	return newMultiFilter;
}

MultiFilter.prototype.GetClassName = function()
{
	return "MultiFilter";
}

function ColumnCounter(columnName, matchingValue)
{
	this.columnName = columnName;
	this.matchingValue = matchingValue;
	this.count = 0;
}


ColumnCounter.prototype.accumulate = function(dataSource)
{
	var data = dataSource.columnsByName[this.columnName].text();
	this.count = 0;
	for (var row = 0; row < data.length; row++)
	{
		if (data[row] == this.matchingValue)
		{
			this.count++;
		}		
	}
}

/*
Sorts the table by the current sorting column.
This function is private.
*/
DataSet.prototype._SortLines = function () 
{
	startTimer("sort", "sort");
	var col = this.sorted_column;
	
	var column = this.columnDefinitions[col];
	var maxWidth = column.max_sort_width;

	var sortData = this._GetColumnSortData(col);

	// sort the array
	sortData.sort();
	
	// reverse if needed	
	if (this.ascending == false) 
	{ 
		sortData.reverse(); 
	}	
	
	// extract the row numbers from the string and 
	// copy the row indices to our sorted list		
	for (var viewRow = 0; viewRow < this.totalEntries; viewRow++)
	{
		var sourceRow = parseInt(sortData[viewRow].substr(maxWidth));
		this.UnfilteredViewToSourceRow[viewRow] = sourceRow;
		this.UnfilteredSourceToViewRow[sourceRow] = viewRow;
	}

	// we are done with this data now
	sortData = null;

	// now update what's in the filter since the order changed
	this._RefreshFilteredLines();

	stopTimer("sort");
}

DataSet.prototype._RefreshFilteredLines = function()
{
	this.FilteredViewToSourceRow.length = 0;
	this.FilteredSourceToViewRow.length = 0;
	
	if (this.IncludedRows == null)
	{
		// we special case the no-filter situation
		this.IncludedRows = new Array();
		for (var i = 0; i < this.totalEntries; i++)
		{
			this.IncludedRows[i] = true;
			this.FilteredViewToSourceRow[i] = this.UnfilteredViewToSourceRow[i];
			this.FilteredSourceToViewRow[i] = this.UnfilteredSourceToViewRow[i];
		}
	}
	else
	{
		for (var i = 0, filteredViewRow = 0; i < this.totalEntries; i++)
		{
			var sourceRow = this.UnfilteredViewToSourceRow[i];
			if (this.IncludedRows[sourceRow])
			{
				// this is in view, so we add it is a view row,
				// and store the reverse mapping as well
				this.FilteredViewToSourceRow[filteredViewRow] = sourceRow;
				this.FilteredSourceToViewRow[sourceRow] = filteredViewRow;
				filteredViewRow++;
			}
			else
			{
				// since this is not in view, we add nothing to this.FilteredViewToSourceRow,
				// and make sure the reverse direction finds nothing				
				this.FilteredSourceToViewRow[sourceRow] = null;
			}
		}
	}	
	
	this.filteredRowCount = this.FilteredViewToSourceRow.length;	
}


// Constructor for DataTable object
// instance_name: the name of this DataTable instance as it is declared in the HTML page
function DataTable(instance_name, format, colDefinitions, dataset) 
{
	this.instance_name = instance_name;
	this.shown_column_count = 0;

	// configuration defaults 
	this.show_filters = false;
	this.show_scrollbar = false;
	this.duplicate_page_controls = false;
	this.editing = false;
		
	// copy in the parameters
	for (var i in format)
	{
		this[i] = format[i];
	}

	if (this.selectable)
	{
		this.table_classes += " selectable";
	}
	
	if (this.color)
	{
		this.table_classes += " " + this.color + "-list";
		if (this.selectable)
		{
			this.table_classes += " " + this.color + "-list-selectable";
		}
	}
	
	// the maximum number of rows we are able to display
	this.MAXIMUM_ROWS = 50;

	this.dataset = dataset;
	
	this.DefineColumns(colDefinitions);

	this.filterInitialized = new Array();
	this.filterPosition = new Array();
	
	this.current_absolute_row = 0;
	
	if (!this.status_function)
	{
		// use the default
		this.status_function = DefaultStatus();
	}

	this.doneActions = [];

	this.buttons = new Array();
	
	var me = this;
	function firstLink(l)
	{
		me.ExecuteNavigationLink(l, 0);
		return true;
	}
	
	function previousLink(l)
	{
		me.ExecuteNavigationLink(l, 1);
		return true;
	}

	function nextLink(l)
	{
		me.ExecuteNavigationLink(l, 2);		
		return true;
	}
	
	function lastLink(l)
	{
		me.ExecuteNavigationLink(l, 3);
		return true;
	}
	
	function addNavigation()
	{
		menuAddItemAction("menu", "js-dt-first-link", firstLink);
		menuAddItemAction("menu", "js-dt-previous-link", previousLink);
		menuAddItemAction("menu", "js-dt-next-link", nextLink);	
		menuAddItemAction("menu", "js-dt-last-link", lastLink);	
	}
	
	addLoadFunction(addNavigation);
}

DataTable.prototype.ReloadData = function(dataset)
{
	this.dataset = dataset;
	this.UpdateAllFilterControls(true);
	this._RestoreState();
	this._ResetRowCache();
	this._Refresh();
}

DataTable.prototype.ExecuteNavigationLink = function(l, navigationIndex)
{
	var state = this.dataset.GetVisibleSelection(null);

	var newViewRow = state.navigationViewRows[navigationIndex];
	newViewRow = Math.min(newViewRow, this.dataset.FilteredSize()-1);
	newViewRow = Math.max(newViewRow, 0);

	// if there's only one row selected, we are navigating through the whole list
	// so the single selection moves with the pointer
	if (this.dataset.GetSelectionCount() == 1)
	{
		this.dataset.SelectExclusive(newViewRow);
	}
	
	this.dataset.MakeViewRowCurrent(newViewRow);
	
	this.SaveState();
	this.SaveSelection();
}

DataTable.prototype.release = function()
{
	for (var n = 0; n < this.buttons.length; n++)
	{
		this.buttons[n].release();
	}
	
	if (this.elevator)
	{
		this.elevator.release();
	}
	
	clearMembers(this.buttons);
	clearMembers(this.tableDataRows);
	for (var row in this.tableDataContainers)
	{
		clearMembers(this.tableDataContainers[row]);
	}
	
	for (var col = 0; col < this.Columns.length; col++)
	{
		if (this.Columns[col].enumeratedImageElements)
		{
			clearMembers(this.Columns[col].enumeratedImageElements);
			this.Columns[col].enumeratedDefaultElement = null;
		}
	}
	
	clearMembers(this);
	
	this.released = true;
//	alert("released");
}

function clearMembers(o)
{
	for (var member in o)
	{
		o[member] = null;
		delete o[member];
	}
}

function statTotalRecordCount(table, el)
{
	el.innerHTML = table.GetTotalSize() + " " + table.GetRowTerm(table.GetTotalSize()) + " total";
}

function statFilteredRecordCount(table, el)
{
	if (table.IsFilterActive())
	{
		el.innerHTML = table.GetFilteredSize() + " in filter";
		if (is_ie)
		{
			// IE doesn't like "table-cell"
			el.style.display = "block";
		}
		else
		{
			el.style.display = "table-cell";
		}		
	}
	else
	{
		el.style.display = "none";
	}
}

function DefaultStatus(table)
{
	return [ statTotalRecordCount ];
}


DataTable.prototype.GetRowTerm = function(count)
{
	if (count == 1)
	{
		return this.row_term_singular;
	}
	else
	{
		return this.row_term_plural;
	}
};

DataTable.prototype.GetFilteredSize = function()
{
	return this.dataset.FilteredSize();
}

DataTable.prototype.GetTotalSize = function()
{
	return this.dataset.TotalSize();
}

DataTable.prototype.GetColumnData = function(colName)
{
	return this.dataset.Get
}

DataTable.prototype.IsFilterActive = function()
{
	return this.dataset.isFilterActive();
}

// Defines the columns.  Takes an array of dictionaries defining each column.
// Each dictionary must have the following attributes:
//   'classes' = space-separated list of the classes to identify this column with
//   'title' = the title of this column (including html if you need it)
//   'numeric' = true if the column is numeric data, false if string data
DataTable.prototype.DefineColumns = function (colDefinitions)
{
	this.Columns = new Array();
	this.columnOrder = new Array();
	this.columnIndexByName = new Object();
	
	for (var i=0; i < colDefinitions.length; i++) 
	{
		var newColumn = new Column(colDefinitions[i], i);
		this.Columns[this.Columns.length] = newColumn;
		this.columnOrder[i] = i;
		this.columnIndexByName[colDefinitions[i].name] = i;
		
		var enumerated = newColumn.enumerated;
		if (enumerated)
		{
			newColumn.enumeratedImages = new Object();
			for (text in enumerated)
			{
				if (enumerated[text].image)
				{
					var image = enumerated[text].image;
					
					if (is_ie)
					{
						// we convert to gifs for all versions of IE because
						// the AlphaImageLoader trick doesn't work well with row-shuffling
						image = image.replace(imageConversionRE, ".gif");
					}
					image = sitePrefix + "/images/list_icons/" + image;

					newColumn.enumeratedImages[text] = image;
				}
			}
		}		
	}
	
	this._UpdateColumns();
};

DataTable.prototype._UpdateColumns = function()
{
	this.visibleColumnIndex = new Array();
	for (var i=0; i < this.columnOrder.length; i++) 
	{
		if (this.Columns[this.columnOrder[i]].visible)
		{
			this.visibleColumnIndex.push(this.columnOrder[i]);
		}
	}
	
	this.shown_column_count = this.visibleColumnIndex.length;
}

DataTable.prototype._ResetVisibleColumns = function()
{
	this.visibleColumnIndex = new Array();
	for (var sourceColNumber = 0; sourceColNumber < this.Columns.length; sourceColNumber++) 
	{
		var column = this.Columns[sourceColNumber];
		column.visible = column.originalVisible;
		if (column.visible)
		{
			this.visibleColumnIndex.push(sourceColNumber);
		}
	}
	
	this.shown_column_count = this.visibleColumnIndex.length;
}

function Column(columnDefinition)
{
	this.visible = true;
	
	// copy in all the properties
	for (def in columnDefinition)
	{
		this[def] = columnDefinition[def];
	}		
	
	this.originalVisible = this.visible;
}


DataTable.prototype.GetDivName = function() 
{
	return "div-" + this.instance_name;
};

DataTable.prototype.GetTableName = function()
{
	return "t-" + this.instance_name;
};
	
DataTable.prototype.GetColumnName = function (i) 
{
	return "col-" + this.instance_name + "-" + i;
};

DataTable.prototype.GetHeaderName = function (i) 
{
	return "h" + this.instance_name + "-" + i;
};


DataTable.prototype.GetFilterControlName = function (col) 
{
	return "fc-" + this.instance_name + "-" + col;
};

// returns the name of the status bar element
DataTable.prototype.GetStatusBarName = function ()
{
	return "dt-status-bar-" + this.instance_name;
};

DataTable.prototype.GetScrollBarName = function()
{
	return this.instance_name + "-scrollbar";
};

DataTable.prototype.GetTopScrollButtonsName = function()
{
	return this.instance_name + "-scroll-top";
};

DataTable.prototype.GetBottomScrollButtonsName = function()
{
	return this.instance_name + "-scroll-bottom";
};

// returns the name of the status bar element
DataTable.prototype.GetSearchBoxName = function ()
{
	return "dt-search-" + this.instance_name;
};


DataTable.prototype.CompleteTable = function()
{
	for (var i = 0; i < this.buttons.length; i++)
	{
		this.buttons[i].initializeEvents();
	}
	
	if (this.elevator)
	{
		this.elevator.initializeEvents();
	}

	// turn on the filter highlights
	this.UpdateAllFilterControls();
	
	if (document.getElementById("datatable_column_box"))
	{
		this.selectColumnsPopup = new PopupDialog("datatable_column_box", "../common/datatable/select_columns.jsp");
	}
	
	var me = this;
	function selectAll()
	{
		me.SelectAllRows();
		return false;
	}
	
	function selectNone()
	{
		me.SelectNoRows();
		return false;
	}
	
	function selectColumns()
	{
		me.SelectColumns();
		return false;
	}

	function addCurrent()
	{
		var state = me.SaveSelection();

		me.dataset.MakeViewRowCurrent(state.currentViewRow);
		me.SaveState();

		var parameter = me.dataset.GetViewRowParameter(state.currentViewRow);

		this.href = mergeParameters(this.originalHref, parameter);
		return true;
	}
	
	function addFirstSelection()
	{
		var state = me.SaveSelection();

		var parameter = me.dataset.GetViewRowParameter(state.firstSelectedViewRow);

		this.href = mergeParameters(this.originalHref, parameter);
		return true;
	}

	function saveSelection()
	{
		me.SaveSelection();
		return true;
	}

	menuAddItemAction("menu", "js-dt-select-all", selectAll);
	menuAddItemAction("menu", "js-dt-select-none", selectNone);	
	menuAddItemAction("menu", "js-dt-select-columns", selectColumns);	
	
	menuAddItemAction("menu", "js-dt-add-current", addCurrent);	
	menuAddItemAction("menu", "js-dt-add-first-selection", addFirstSelection);	
	menuAddItemAction("menu", "js-dt-selection-required", saveSelection);	
	menuAddItemAction("menu", "js-dt-one-selection-required", saveSelection);	
	
	if (this.editing)
	{
		this.MakeEditable();
	}		
};


DataTable.prototype.AttachRowEvents = function(row, rowNumber)
{
	var me = this;
	
	function select(e)
	{
		if (!e) var e = window.event;
		if (e.shiftKey)
		{
			if (me.multiselect)
			{
				me.SelectIntermediateRows(rowNumber, !e.ctrlKey);
			}
		}
		else if (e.ctrlKey)
		{
			if (me.multiselect)
			{
				me.SelectRowAdditive(rowNumber);
			}
		}
		else
		{
			me.SelectRowExclusive(rowNumber);
		}
	}
	
	// calls the row_activate function if it was set.
	// passes it the value of the 'row_parameter' column 
	// from the current row if row_parameter was defined,
	// or the row number if not.
	// rowNumber: the offset from the top of the table
	function activate(e)
	{
		if (!e) var e = window.event;
		if (!e.ctrlKey && !e.shiftKey)
		{
			me.ActivateRow(rowNumber);
		}
	}
		
	if (this.selectable)
	{
		row.onclick = select;
	}
	
	if (!this.editing && this.selectable)
	{
		row.ondblclick = activate;
	}
	
	// break the closure on element so we don't leak memory in IE
	row = null;
}


// Returns the element for the given column with the given text.
// This will be either an image element 
DataTable.prototype.GetIcon = function(column, text)
{
	var imageElement = column.enumeratedDefaultElement;
	if (text)
	{
		// see if this text is known to us
		var imageSrc = column.enumeratedImages[text];
		if (imageSrc)
		{
			// there is an icon for this text, so see if we have a cached image element
			imageElement = column.enumeratedImageElements[text];
			if (!imageElement)
			{
				// make a new image element for our given source
				imageElement = document.createElement("img");
				imageElement.src = imageSrc;
				imageElement.alt = "";
			}
			
			// cache this new image
			column.enumeratedImageElements[text] = imageElement;
		}
	}

	// return a copy of this element, whether we used the cached element or created our own
	return imageElement.cloneNode(true);
}


DataTable.prototype.GetEditableCellHTML = function(rowData, id, absoluteViewRow, viewColNumber)
{
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	var val = rowData[sourceColNumber].text;

	var html = new Array();
	
	html[html.length] = "<input onselectstart='stopPropagation(window.event); return true' type='text'";
	var arguments = "this," + absoluteViewRow + "," + viewColNumber;
	html[html.length] = " onChange='list_table.ValueChanged(" + arguments + ")'";
	html[html.length] = " onFocus='list_table.SaveOriginalInputValue(" + arguments + ")' onBlur='list_table.ForgetOriginalInputValue()'";
	// the name includes the row to make it unique
	html[html.length] = " name=";
	
	var column = this.Columns[sourceColNumber];
	html[html.length] = (column.name + "(" + absoluteViewRow + ")").quote();
	html[html.length] = " class='js-immediate-onchange'";
	var maxDisplayLength = column.max_display_length;
	if (maxDisplayLength)
	{
		html[html.length] = " size=";
		html[html.length] = String(maxDisplayLength + 4).quote();
	}
	
	var maxLength = column.max_length;
	if (maxLength)
	{
		html[html.length] = " maxlength=";
		html[html.length] = String(maxLength).quote();
	}
	
	var status = rowData[0].text;
	// the status column is hardcoded
	if (status.indexOf("D") != -1)
	{
		html[html.length] = " disabled ";
	}
	
	html[html.length] = " value=";
	html[html.length] = val.htmlQuote();
	html[html.length] = ">";
	
	if (viewColNumber == 1)
	{
		html[html.length] = "<input type='hidden'";
		html[html.length] = " name=";
		// the name includes the row to make it unique
		html[html.length] = ("id(" + absoluteViewRow + ")").quote();			
		html[html.length] = " value='";
		html[html.length] = id;
		html[html.length] = "'>";
		
		html[html.length] = "<input type='hidden'";
		html[html.length] = " name=";
		// the name includes the row to make it unique
		html[html.length] = ("status(" + absoluteViewRow + ")").quote();
		html[html.length] = " value='";
		html[html.length] = status;
		html[html.length] = "'>";
	}			

	return html.join("");
}

DataTable.prototype.InitializeTable = function()
{
	this.tableElement = document.getElementById(this.GetTableName());
	var table = this.tableElement;
	
	// initialize all the enumeration for all columns
	for (var sourceColNumber = 0; sourceColNumber < this.Columns.length; sourceColNumber++)
	{
		var column = this.Columns[sourceColNumber];
		if (column.enumeratedImages)
		{
			// make an empty map for all the icons
			column.enumeratedImageElements = {};
			
			// make a filler element for rows without icons
			column.enumeratedDefaultElement = document.createElement("div");
			column.enumeratedDefaultElement.innerHTML = nonbreakingSpace;
			column.enumeratedDefaultElement.className = "image-filler";			
		}
	}	
	
	this.tableDataRows = [];
	this.tableDataContainers = [];
	for (var rowIndex = 0, len = table.rows.length - table.tHead.rows.length; rowIndex < len; rowIndex++)
	{
		var row = table.rows[rowIndex + table.tHead.rows.length];
		this.tableDataRows[this.tableDataRows.length] = row;

		this.tableDataContainers[rowIndex] = [];
		for (var col = 0; col < row.cells.length; col++)
		{
			var cell = row.cells[col];
			while (cell && cell.nodeType != 3)
			{
				cell = cell.firstChild;
			}
			
			this.tableDataContainers[rowIndex][col] = cell.parentNode;
		}

		this.AttachRowEvents(row, rowIndex);
		
		if (this.editing)
		{
			scanElement(row);
		}	
	}
	
	if (this.editing)
	{
		attachKeyToButton([27], "cancel_button");
	}
}


// Print out the original set of rows in the grid
DataTable.prototype.CreateTable = function () 
{
//	debuggingEnabled = false;
	
	
	startTimer("tablestart", "table code time");
	
	debuggingEnabled = false;
	
	
	var html = "";
	if (this.show_scrollbar)
	{
		html += "<div class='dt-scrollbar-wrapper'>";
	}
		
	html += "<div class='data-table' id='" + this.GetDivName() + "'>";
	
	html += "</div>";

	if (this.show_scrollbar)
	{
		html += this.WriteScrollBar();
	}
	
	if (this.show_status_bar)
	{
		html += this.WriteStatusBar();
	}
		
	if (this.show_scrollbar)
	{
		html += "</div>";
	}
	
	var container = GetElement(this.container_element_id);
	container.innerHTML = html;

	var tableDiv = GetElement(this.GetDivName());
	startTimer("createtable", "CreateTable wrapper");
	startTimer("createtable1", "CreateTableHTML");
	html = this.CreateTableHTML();
	stopTimer("createtable1");

	startTimer("createtable2", "CreateTable innerHTML");
	tableDiv.innerHTML = html;
	stopTimer("createtable2");
	stopTimer("createtable");

	// make sure we get unloaded at the end
	addActiveElement(this);

	startTimer("inittable", "InitializeTable");
	this.InitializeTable();
	stopTimer("inittable");

	startTimer("complete", "CompleteTable");
	this.CompleteTable();
	stopTimer("complete");

	this.Resize();
	makeResizable(this);

	startTimer("external", "after");
	window.setTimeout(function() { 	stopTimer("external"); }, 1);	
		
	window.setTimeout(function() { 	stopTimer("tablestart"); }, 1);	
	debuggingEnabled = true;
}


function makeResizable(table)
{
	// this is in a separate function to avoid closures on 
	// elements we don't intend
	addResizableItem(function() { table.Resize(); });
}

DataTable.prototype.CreateTableHTML = function ()
{
	var html = "";
	html += "<table class='" + this.table_classes;
	if (this.editing)
	{
		html += " editing";
	}
	
	html += "' id='" + this.GetTableName() + "' border='0' cellspacing='0' cellpadding='0'>";

	for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++) 
	{
		html += "<col id='" + this.GetColumnName(viewColNumber) + "'>";
	}
	
	html += "<thead><tr>";

	for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++) 
	{
		var sourceColNumber = this.visibleColumnIndex[viewColNumber];
		var column = this.Columns[sourceColNumber];
		html += "<th valign='bottom' class='" + this.GetHeaderStyle(viewColNumber) + "'";
		
		var title = "";
		if (column.description)
		{
			title += column.description;
		}
		else
		{
			title += column.title;
		}				

		if (is_ie)
		{
			title += "\n";
		}
		else
		{
			title += " ";
		}

		if (this.sortable)
		{
			title += "(Click to sort by this column)";
		}
		else
		{
			title += "(This column is not sortable)";
		}
					
		html += " title='" + title + "'";
		
		html += " id='" + this.GetHeaderName(viewColNumber);

		// this div is needed to keep IE6 from truncating part of the link when 
		// table-wide classes are changed
		html += "'><div><a";
		if (this.sortable)
		{
			html += " href='javascript:dtSortRows(" + this.instance_name + ", " + viewColNumber + ")'";
		}
		
		html += ">";
		
		if (column.title_icon)
		{
			html += "<span class='title-icon' style='background-image: url(" +  sitePrefix  + "/images/heading_icons/" + column.title_icon + ")'>";
			html += nonbreakingSpace;
			html += "</span>";
		}
		else if (column.title)
		{
			html += "<span class='title-text'>";
			html += column.title;
			html += "</span>";
		}
		
		html +="</a></div></th>";
	}
	
	html += "</tr>";

	if (this.show_filters)
	{
		html += this.WriteFilterRow();
	}
	
	html += "</thead>";
	
	
	this.MAXIMUM_ROWS = 33;
		
//	startTimer("inner", "inner");
	
	var htmlArray = new Array();

	var len = 0;
	var numCols = this.visibleColumnIndex.length;
	var numRows = this.MAXIMUM_ROWS;
	for (var tr_number = 0; tr_number < numRows; tr_number++) 
	{
		htmlArray[htmlArray.length] = "<tbody><tr onselectstart='return false' class='row-";
		htmlArray[htmlArray.length] = (tr_number % this.unique_row_styles);
		htmlArray[htmlArray.length] = "' id='row-";
		htmlArray[htmlArray.length] = tr_number;
		htmlArray[htmlArray.length] = "'";

		if (tr_number >= this.rows_per_page)
		{
			htmlArray[htmlArray.length] = " style='display: none'";
		}

		htmlArray[htmlArray.length] = ">";

		for (var viewColNumber = 0; viewColNumber < numCols; viewColNumber++) 
		{
			htmlArray[htmlArray.length] = "<td class='";
			htmlArray[htmlArray.length] = this.GetDataCellStyle(viewColNumber);
			htmlArray[htmlArray.length] = "'>";
			htmlArray[htmlArray.length] = "<div>";
			
			var sourceColNumber = this.visibleColumnIndex[viewColNumber];
			var column = this.Columns[sourceColNumber];
	
			if (this.editing && column.editable)
			{
				htmlArray[htmlArray.length] = nonbreakingSpace;
			}
			else if (column.enumeratedImages)
			{
				// this is just filler for the image element that will be replaced,
				// once we create the elements from this HTML
				htmlArray[htmlArray.length] = nonbreakingSpace;
			}
			else
			{
				if (is_ie && ie_version < 5.5)
				{
					// a total kludge for IE5: use nobr in place of the CSS white-space attribute,
					// and wrap the contents in a link so that it properly ignores selections when you ctrl-click.
					htmlArray[htmlArray.length] = "<nobr><a href='#' onmousedown='stopPropagation()' onfocus='blur()' onselectstart='return false' onclick='return false'>";
					
					htmlArray[htmlArray.length] = nonbreakingSpace;
					htmlArray[htmlArray.length] = "</a></nobr>";
				}
				else
				{
					htmlArray[htmlArray.length] = nonbreakingSpace;
				}	
			}
		
			htmlArray[htmlArray.length] = "</div>";
				
			htmlArray[htmlArray.length] = "</td>";
		}

		htmlArray[htmlArray.length] = "</tr></tbody>";
	}

//	stopTimer("inner");

	html += htmlArray.join("");
	html += "</table>";
	return html;
}

DataTable.prototype.WriteFilterRow = function()
{
	var html = "<tr class='toolbar'>";

	// currently no active filter
	for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++) 
	{
		var sourceColNumber = this.visibleColumnIndex[viewColNumber];
		var column = this.Columns[sourceColNumber];

		this.filterInitialized[viewColNumber] = false;

		// always start with filters at the top
		this.filterPosition[viewColNumber] = 0;

		html += "<th class='dt-filter'>";
		
		if (column.filter_type == "value")
		{
			html += this.WriteFilterSelect(viewColNumber);
		}
		else if (column.filter_type == "toggle")
		{
			html += this.WriteFilterButton(viewColNumber);
		}
		else
		{
			html += "&nbsp;";
		}
		html += "</th>";
	}	
	
	html += "</tr>";
	
	return html;
};

DataTable.prototype.WriteFilterSelect = function(viewColNumber)
{
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	var column = this.Columns[sourceColNumber];

	// set/remove the filter when the user picks something
	var html = "<select onChange='javascript:dtChangeFilter(";
	html += this.instance_name;
	html += ", \"";
	html += column.name;
	html += "\", this)' id='";
	html += this.GetFilterControlName(viewColNumber);
	html += "' onMouseOver='javascript:dtInitializeSelect(";
	html += this.instance_name;
	html += ", ";
	html += viewColNumber;
	html += ")'>";
	
	var f = this.GetColumnFilter(column.name);
	if (f)
	{
		// include this option, though not selected
		html += " <option>(All)</option>";
		
		html += "<option selected>";
		html += f.GetMatchText();
		html += "</option>";
	}
	else
	{
		// this option is here as a filler--we will initialize the rest
		// of the selects on mouseover
		html += " <option selected>(All)</option>";
	}
	
	if (column.filter)
	{
		// add the longest item after the displayed item so that the select width can be fixed
		var maxitem = this.Columns[sourceColNumber].max_display_value;
			
		html += "<option>";
		html += maxitem;
		html += "</option>";
	}
	
	html += "</select>";
	
	return html;
};

function dtToggleFilterButton(table, columnName, value)
{
	if (dtIsColumnFilterEnabled(table, columnName))
	{
		dtRemoveColumnFilter(list_table, columnName);
	}
	else
	{
		dtSetColumnMatchFilter(list_table, columnName, value)
	}

	return false;
}


DataTable.prototype.WriteFilterButton = function(viewColNumber)
{
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	var column = this.Columns[sourceColNumber];

	var filter = column.toggle_filter;
	var html = "<a href='#' class='";
	html += "icon-button toggle-button icon-only " + column.name + "-icon";
	html += "' id='";
	html += this.GetFilterControlName(viewColNumber);
	html += "' onClick='dtToggleFilterButton(" + this.instance_name + ", \"" + column.name + "\", \"" + filter.value + "\"); return false'>";
	html += "</a>";
	
	return html;	
};

DataTable.prototype.WriteScrollBar = function ()
{
	var html = "<div class='dt-scrollbar' id='" + this.GetScrollBarName() + "'>";
	html += "<div class='dt-page-top' id='" + this.GetTopScrollButtonsName() + "'>";
	html += new ScrollButton(this, -1, "prev-row").getHtml();
	html += "</div>";
	
	html += new ScrollElevator(this).getHtml();
	
	html += "<div class='dt-page-bottom' id='" + this.GetBottomScrollButtonsName() + "'s>";
	html += new ScrollButton(this, 1, "next-row").getHtml();
	html += new ScrollButton(this, -this.rows_per_page, "prev-page", "Page Up").getHtml();
	html += new ScrollButton(this, this.rows_per_page, "next-page", "Page Down").getHtml();
	html += "</div></div>";

	return html;
};

DataTable.prototype.WriteStatusBar = function ()
{
	this.statusBarFunctions = this.status_function(this);
	
	// just make the table structure, with no rows
	var html = "<div class='dt-status-bar'><table class='dt-status-bar toolbar'><tr id='";
	html += this.GetStatusBarName();
	html += "'>";
	
	for (var i = 0; i < this.statusBarFunctions.length; i++)
	{
		// this is for IE5, which doesn't understand the CSS white-space: nowrap style.
		html += "<td class='dynamic' nowrap></td>";
	}
	html += "</tr></table>";
	
	if (this.show_filters || this.editing)
	{
		html += "<div style='float: right'>";
		if (this.show_filters)
		{
			html += "<span title='Quickly find rows with all words in any order'><span class='search-label'>Search:";
			if (is_ie && ie_version < 5.5)
			{
				// IE 5.0 won't add margin or padding to inline elements
				html += "&nbsp;";
			}		
		
			html += "</span><input type='text' id='";
			html += this.GetSearchBoxName();
		
			var f = this.GetRowFilter("searchBox");
			if (f)
			{
				html += "' value='";
				html += f.GetMatchText();
			}
		
			html += "'></span>";
		}

		if (this.editing)
		{
			html += "<input type='submit' value='OK' class='standard-button'>";
			html += "<input type='button' name='cancel_button' value='Cancel' onClick='closeDialogBox(this.form, \"Discard your changes to these ";
			html += this.row_term_plural;
			html += "?\", true)' class='standard-button'>";
		}	
		else
		{
		/*
			if (isProduction == false)
			{
				html += "<input type='button' value='(R)'";
				html += " onClick='followGivenLink(\"" + this.reload_page + "\", \"datatable_reload\")'>";
			}*/
		}		
		
		html += "</div>";
	}
	
	html += "<br class='wrap'></div>";
	return html;
};

DataTable.prototype.UpdateScrollBar = function()
{
	for (var index = 0; index < this.buttons.length; index++)
	{
		this.buttons[index].updateState();
	}
	
	if (this.elevator)
	{
		this.elevator.updateState();
	}
};

function updateSearch(el)
{
	var el = this;
	// IE fires this event AFTER the tables have been unloaded,
	// if the cursor is in the search box
	if (!list_table.released)
	{
		if (el.value.length == 0)
		{
			dtRemoveRowFilter(list_table, "searchBox");
		}
		else
		{
			dtSetRowFilter(list_table, "searchBox", el.value);
		}
	}
	el.defaultValue = el.value;
}

var searchElement = null;

DataTable.prototype.UpdateStatusBar = function()
{
	var tr = document.getElementById(this.GetStatusBarName());
	
	for (var i = 0, len = tr.cells.length; i < len; i++)
	{
		this.statusBarFunctions[i](this, tr.cells[i]);
	}

	if (this.show_filters && !searchElement)
	{
		searchElement = document.getElementById(this.GetSearchBoxName());
		searchElement.onchange = updateSearch;
		searchElement.onfocus = startImmediateOnChange;
		searchElement.onblur = stopImmediateOnChange;
		// disable submit on enter
		searchElement.onkeypress = disableEnter;	
	}
};

function parseSpacing(spacing)
{
	spacing = parseInt(spacing);
	if (isNaN(spacing))
	{
		// we got a string value here, so we treat that as zero
		return 0;
	}
	else
	{
		return spacing;
	}
}


DataTable.prototype._RefreshAccumulators = function()
{
	for (a in this.accumulators)
	{
		this.accumulators[a].accumulate(this.dataset.dataSource);
	}
}

DataTable.prototype.Resize = function()
{
	startTimer("resize", "Resize");
	this.ResizeHorizontal();
	this.ResizeVertical();
	this.ResizeScrollbar();
	this.ResizeAdditional();
	stopTimer("resize");
}

DataTable.prototype.ResizeScrollbar = function()
{
	if (this.show_scrollbar)
	{
		var divElement = GetElement(this.GetDivName());
		var scrollbarElement = GetElement(this.GetScrollBarName());
		scrollbarElement.style.height = divElement.offsetHeight + "px";
		scrollbarElement.style.display = "block";

		// for some reason, after resizing, Mozilla forgets that the css declaration sets the right position.
		// no other browser needs this. If you set "right" properly, though, it messes up IE and Opera.
		// So instead we set left since we can compute the width of the containing block.
		var parent = scrollbarElement.parentNode;
		var position = GetElementWidth(parent) - GetElementWidth(scrollbarElement);
		scrollbarElement.style.left = position + "px";
		
		this.elevator.resize();
	}
}

DataTable.prototype.ResizeAdditional = function()
{
	if (this.selectColumnsPopup && this.selectColumnsPopup.visible)
	{
		this.ResizeSelectColumnsDialog();
	}		
}

DataTable.prototype.ResizeVertical = function()
{
	// start with the padding/margin on the element which has all the space	
	var sizedElement = GetElement(this.vertical_bound_element_id);
	var usedHeight = computeVerticalSpacing(sizedElement);
	
	// now add up the sizes of all children (which assumes that they are stacked on
	// top of each other--an assumption that may not be true).  To make it
	// true, you could wrap each set of floated elements in a div so that the children
	// of this element are all stacked vertically.
	var children = sizedElement.childNodes;
	for (var n = 0; n < children.length; n++)
	{
		var child = children[n];
		if (child.nodeType == 1)
		{
			usedHeight += GetElementHeight(child);	
		}
	}

	// now we pretend that the table has no rows, so that we can compute the total 
	// amount of space we have free, not just the space we have free with the current
	// table.  This is far easier than removing all the rows and getting the browser
	// to update properly.  We do this by just computing the space they take up.
	// note: this assumes all rows are the same height.
	var rowHeight = GetElementHeight(this.tableDataRows[0]);
	usedHeight -= rowHeight * this.rows_per_page;
	
	// now, for the available space, start with the space we have: the window height minus the padding/margin on this element.
	// (note: this may not be truly the interior size of the element if the element does 
	// not take up 100% of the window, so this is not a perfect algorithm).
	// But we use the window height so that shrinking the window will contract the table,
	// which would not happen if we used the size of the element.
	var availableHeight = getWindowHeight();
	
	// remove the used height, and what's left is the space we have to play with
	availableHeight -= usedHeight;

	// now set the row count based on the available height	
	var availableRows = Math.floor(availableHeight / rowHeight);
	this.SetRowCount(availableRows);
	
	// mozilla seems to like having the height reset to "auto"
	// to make it redraw things.  That's what it is, so it shouldn't be necessary,
	// but it is.
	if (!is_ie)
	{
		var tableElement = GetElement(this.GetTableName());
		for (var currentElement = tableElement; currentElement != sizedElement.parentNode; currentElement = currentElement.parentNode)
		{
			currentElement.style.height = "auto";
		}
	}
};

function describeElement(element)
{
	var desc = element.nodeName;
	if (element.id)
	{
		desc += " " + element.id;
	}
	
	if (element.className)
	{
		desc += " " + element.className;
	}
	
	return desc;
}


DataTable.prototype.ResizeHorizontal = function()
{
	var sizedElement = GetElement(this.horizontal_sized_element_id);
	var divElement = GetElement(this.GetDivName());

	var elements = new Array();
	var accumulatedHorizontalSpacing = 0;
	
	for (var currentElement = divElement; true; currentElement = currentElement.parentNode)
	{
		elements[elements.length] = currentElement;	

		accumulatedHorizontalSpacing += computeHorizontalSpacing(currentElement);
		
		if (currentElement == sizedElement)
		{
			// we don't leave for the sized element until AFTER we added it to the list
			break;
		}
	}

	// the space that the div can take up...we never let any of the divs from
	// the table out to the container element get wider than this	
	var containerWidth = getBodyWidth() - GetAbsoluteLeft(sizedElement);
	var availableDivWidth = containerWidth - accumulatedHorizontalSpacing;

	// a kludge for mozilla, which doesn't seem to compute the above
	// the same as IE.
	availableDivWidth -= 20;
		
	// the full width of all the columns in the table...we never stretch the 
	// table out more than this.
	var tableElement = GetElement(this.GetTableName());
	var neededTableWidth = GetElementWidth(tableElement);

	// the width of the div containing the table is the smaller of:
	// how wide the table needs to be, and how much space is available
	var actualDivWidth = Math.min(neededTableWidth, availableDivWidth);

	// by default, the table width is whatever is needed, since we
	// don't shrink the table
	var actualTableWidth = neededTableWidth;
	
	// if there is a min width which we have to stretch the table to meet, AND there is room for it,
	// then that both constrains the div and stretches the table
	if (this.min_width && this.min_width > actualTableWidth && this.min_width < availableDivWidth)
	{
		actualDivWidth = this.min_width;
		actualTableWidth = this.min_width;
	}
		
	if (actualTableWidth != neededTableWidth)
	{
		// the table then gets its width, but only if needs to (for speed)
		tableElement.style.width = actualTableWidth + "px";
	}

	// and all the divs surrounding the table get their width
	var width = actualDivWidth;
	if (!is_ie)
	{
		// Mozilla needs to have the width set on all elements
		for (var i = elements.length-1; i >= 0; i--)
		{
			elements[i].style.width = width + "px";
		}	
	}	
	else
	{
		elements[0].style.width = width + "px";
	}
};


var DATATABLE_COOKIE_PREFIX = "datatable-";

DataTable.prototype.GetCookieName = function ()
{
	return DATATABLE_COOKIE_PREFIX + escape(this.unique_name);
};

var DATATABLE_LAYOUT_COOKIE_PREFIX = "dtl-";

DataTable.prototype.GetLayoutCookieName = function ()
{
	return DATATABLE_LAYOUT_COOKIE_PREFIX + escape(this.layout);
};


DataTable.prototype.MakeRoomForNewCookie = function ()
{	
	var oldestCookieName = null;
	// the oldest time is now, because we know nothing will be later than that
	var oldestTime = new Date().getTime();
	
	// start with one cookie since we know we will be adding one after this function
	var cookieCount = 1;

	// this is a small limit to keep the browser from kicking out one of our important 
	// cookies (like for session tracking). The spec says browsers are only expected to hold 20 cookies per server.
	var MAX_COOKIE_COUNT = 5;
	var tableNameStart = DATATABLE_COOKIE_PREFIX.length;
	
	// cookies are terminated by semicolons (some browsers add spaces)
	var cookies = document.cookie.split(/\s*;\s*/);
	for (var i = 0; i < cookies.length; i++)
	{
		// each cookie is name=value
		var result = cookies[i].split("=");
		var thisName = result[0];
		var thisValue = unescape(result[1]);
		// see if this is one of our cookies
		if (thisName.slice(0, tableNameStart) == DATATABLE_COOKIE_PREFIX)
		{
			// this cookie is one of ours, so we count it
			cookieCount++;
		
			// get the time and compare it to our oldest
			var state = deserializeState(thisValue);
			var thisTime = parseInt(state["time"]);
			if (thisTime < oldestTime)
			{
				// found a new oldest cookie
				oldestTime = thisTime;
				oldestCookieName = thisName;
			}
		}
	}
	
	// only delete the cookie if we are out of space
	if (oldestCookieName && cookieCount > MAX_COOKIE_COUNT)
	{
		// we have too many cookies, so we remove the oldest cookie 
		removeCookie(oldestCookieName);
	}
};
	
DataTable.prototype.SetCookie = function (state, name)
{
	// as an optimization, we check to see if we have already set a cookie with
	// the same name--if we don't, we might need to remove one, since we will be adding one
	if (!findCookie(name))
	{
		this.MakeRoomForNewCookie();
	}
	
	setPermanentCookie(name, state);
};


// automatically fix any cookies in the old format
dtAdjustCookies();

var CURRENT_DT_VERSION = 1;
DataTable.prototype.SaveState = function ()
{
	if (this.preserve_state)
	{
		var state = new Object();
		state["ver"] = CURRENT_DT_VERSION;
		state["rev"] = this.dataset.currentRevision;
		state["row"] = this.current_absolute_row;
		state["f"] = this.dataset.GetFilterState();
		state["so"] = (this.dataset.IsAscending() ? "1" : "0");
		state["sc_name"] = this.Columns[this.dataset.GetSortedColumn()].name;
		state["sel"] = this.dataset.GetSelectionState();
		state["cur"] = this.dataset.GetCurrentSourceRow();
		state["time"] = new Date().getTime();

		var new_state = serializeState(state);
		
		this.SetCookie(new_state, this.GetCookieName());
	}
	
	this.UpdateMenu();
};

var CURRENT_DT_LAYOUT_VERSION = 1;

DataTable.prototype.SaveLayoutState = function (columnLayout)
{
	if (this.layout)
	{
		var state = new Object();
		state["ver"] = CURRENT_DT_LAYOUT_VERSION;
		state["cols"] = columnLayout;
		var new_state = serializeState(state);

		setPermanentCookie(this.GetLayoutCookieName(), new_state);
	}
};


DataTable.prototype.UpdateMenu = function()
{
	menuSetItemState("menu", "js-dt-delayed", true);

	// check these first, so they can be overridden later
	menuSetItemState("menu", "js-dt-add-current", this.dataset.FilteredSize() > 0);	
	menuSetItemState("menu", "js-dt-add-first-selection", this.dataset.FilteredSize() > 0);	

	menuSetItemState("menu", "js-dt-select-all", this.dataset.FilteredSize() > 0);
	menuSetItemState("menu", "js-dt-select-none", this.dataset.FilteredSize() > 0);	

	menuSetItemState("menu", "js-dt-undo-required", this.doneActions.length > 0);
	
	menuSetItemState("menu", "js-dt-deleted-selection-required", this.SelectionIncludesDeleted(false));
	menuSetItemState("menu", "js-dt-undeleted-selection-required", this.SelectionIncludesDeleted(true));
	
	menuSetItemState("menu", "js-dt-selection-required", this.dataset.GetSelectionCount() > 0);

	menuSetItemState("menu", "js-dt-one-selection-required", this.dataset.GetSelectionCount() == 1);
}

DataTable.prototype.SaveSelection = function ()
{
	var state = this.dataset.GetVisibleSelection(" ");
	setPermanentCookie("selection", state.selection);
	
	var sort = this.dataset.GetSortedColumn() + (this.dataset.IsAscending() ? "+" : "-");
	setPermanentCookie("selection-sort", sort);
	
	setPermanentCookie("navigation", state.navigationIDs.join("&"));

	return state;
};


// this function and everything that is only called by it 
// can be deleted 30 days after it is running on the live site.
// By that time, any cookies that have not been adjusted
// have expired (and we can leave a stub function that simply 
// deletes old cookies rather than converts them).
//
// clock started 2004-12-14.
// 2005-01-14: changed to just delete old cookie
function dtAdjustCookies()
{
	var tableNameStart = DATATABLE_COOKIE_PREFIX.length;
	
	// cookies are terminated by semicolons (some browsers add spaces)
	var cookies = document.cookie.split(/\s*;\s*/);
	for (var i = 0; i < cookies.length; i++)
	{
		// each cookie is name=value
		var result = cookies[i].split("=");
		var thisName = result[0];
		var thisValue = unescape(result[1]);
		// see if this is one of our cookies
		if (thisName.slice(0, tableNameStart) == DATATABLE_COOKIE_PREFIX)
		{
			if (thisValue.indexOf("!") != -1)
			{
				removeCookie(thisName);
			}
		}
	}	
}

function clearStateCookie(name, value)
{
	// first log this cookie's contents
	addErrorDetail("offending-cookie-name", name);
	addErrorDetail("offending-cookie-value", value);
	
	// remove this cookie, since it caused the problem
	removeCookie(name);
	
	// we no longer want this as an error handler
	errorHandlers.pop();
	
	return false;
}

function clearLayoutCookie(name, value)
{
	// first log this cookie's contents
	addErrorDetail("offending-cookie-name", name);
	addErrorDetail("offending-cookie-value", value);
	
	// remove this cookie, since it caused the problem
	removeCookie(name);
	
	// we no longer want this as an error handler
	errorHandlers.pop();
	
	return false;
}

DataTable.prototype._RestoreState = function ()
{
	var restored = false;
	var sorted = false;
	
	if (this.preserve_state)
	{
		var cookieName = this.GetCookieName();
		var cookieValue = findCookie(cookieName);
		if (cookieValue)
		{
			errorHandlers.push(function() { clearStateCookie(cookieName, cookieValue) });
			
			var state = deserializeState(cookieValue);
			
			// do not read in cookies that have an older version than we can handle
			if (parseInt(state["ver"]) >= CURRENT_DT_VERSION)
			{
				this.current_absolute_row = parseInt(state["row"]);
				this.current_absolute_row = Math.min(this.current_absolute_row, this.dataset.FilteredSize() - this.rows_per_page - 1);
				this.current_absolute_row = Math.max(this.current_absolute_row, 0);
				
				var currentRow = null;
				if (state["cur"] != "null")
				{
					currentRow = parseInt(state["cur"]);
					if (isNaN(currentRow))
					{
						currentRow = null;
					}
				}		

				this.dataset.RestoreSelections(state["sel"], currentRow, state["rev"]);

				this.dataset.LoadFilter(state["f"]);
				this._RefreshAccumulators();

				var sortColumn;
				if (state["sc_name"])
				{
					sortColumn = this.columnIndexByName[state["sc_name"]];
				}
				else
				{
					// restore the old column for backwards compatibility
					sortColumn = parseInt(state["sc"]);
				}
				
				if (typeof(sortColumn) != "undefined")
				{
					this.dataset.SetSortOrder(sortColumn, parseInt(state["so"]) == 1);
					sorted = true;
				}
				
				restored = true;
			}
			errorHandlers.pop();
		}
	}

	if (!sorted)	
	{
		// sort this by the default, but only if we didn't get a cookie
		// so we don't sort twice
		this.dataset.SetSortOrder(this.dataset.sorted_column, this.dataset.ascending);
	}
	
	if (!restored)
	{
		this._RefreshAccumulators();
	}	
};


DataTable.prototype._RestoreLayoutState = function ()
{
	if (this.layout)
	{
		var cookieName = this.GetLayoutCookieName();
		var cookieValue = findCookie(this.GetLayoutCookieName());
		if (cookieValue)
		{
			errorHandlers.push(function() { clearLayoutCookie(cookieName, cookieValue) });
			
			var state = deserializeState(cookieValue);
			
			// do not read in cookies that have an older version than we can handle
			if (parseInt(state["ver"]) >= CURRENT_DT_LAYOUT_VERSION)
			{
				this.ExtractLayout(state["cols"]);
			}
			errorHandlers.pop();
		}
	}
};


DataTable.prototype.ExtractLayout = function(state)
{	
	// extract the state of the columns
	this.columnOrder = new Array();	
	var columns = state.split(",");

	var hasColumns = false;
	for (var col = 0; col < columns.length; col++)
	{
		var parts = columns[col].split("=");
		var name = unescape(parts[0]);
		var sourceColNumber = this.columnIndexByName[name];
		this.columnOrder.push(sourceColNumber);
		
		var visible = (parts[1] == "1");
		this.Columns[sourceColNumber].visible = visible;

		if (visible) hasColumns = true;
		
		// remove filters for hidden columns
		if (!visible && this.isColumnFiltered(name))
		{
			this.RemoveColumnFilter(name);
		}
	}
	
	if (hasColumns == false)
	{
		// if there are no visible columns, we reset it to show the original state
		// rather than trying to deal with no columns
		this._ResetVisibleColumns();
	}
	else
	{
		this._UpdateColumns();	
	}
}


function dtInitializeSelect(table, viewColNumber)
{
	if (table.filterInitialized[viewColNumber] === false)
	{
		table.UpdateFilterSelect(viewColNumber);
	}	
}

function dtChangeFilter(table, viewColNumber, select)
{
	select.blur();
	window.focus();
	if (select.selectedIndex === 0)
	{
		// this is always at 0
		table.filterPosition[viewColNumber] = 0;

		// remove filter
		dtRemoveColumnFilter(table, viewColNumber);
	}
	else
	{	
		// this is always at 1, since we will remove all other entries
		table.filterPosition[viewColNumber] = 1;
		
		// set filter
		var entry = select.options[select.selectedIndex].value;
		dtSetColumnMatchFilter(table, viewColNumber, entry);
	}	
}



DataTable.prototype.SelectColumns = function()
{	
	clearDebugLog();

	var me = this;
	this.selectColumnsPopup.setCallback(function() { me.InitSelectColumnsPopup(); });
	this.selectColumnsPopup.load();
}

DataTable.prototype.InitSelectColumnsPopup = function()
{
	var el = datatable_column_box.document.getElementById("datatable-column-checklist-container");
	if (el)
	{
		var id = "dtcol";

		var html = "";
		// we build this with HTML strings because otherwise IE doesn't properly 
		// add the checkboxes to the form.elements property
		html += "<ul class='checklist commutable dt-select-columns' id='dtcol'>";
		for (var colNumber = 0; colNumber < this.columnOrder.length; colNumber++)
		{
			var sourceColNumber = this.columnOrder[colNumber];
			var column = this.Columns[sourceColNumber];

			html += "<li><input type='checkbox' name='";
			html += id;
			html += "' id='";
			html += (id + colNumber);
			html += "' value='";
			html += this.Columns[sourceColNumber].name;
			html += "'";
			
			if (column.visible)
			{
				// IE likes to see "defaultChecked" here
				html += "checked";
			}

			html += "><label>";

			var title = column.title;
			title = title.replace(/&nbsp;/g, " ");
			html += title;
			html += "</label></li>";
		}
		
		html += "</ul>";
		el.innerHTML = html;
		
		var ul = this.selectColumnsPopup.document.getElementById(id);
		new this.selectColumnsPopup.scope.Checklist(ul);

		this.ResizeSelectColumnsDialog();
	}
};


DataTable.prototype.ResizeSelectColumnsDialog = function()
{
	this.selectColumnsPopup.show();
	var el = document.getElementById(this.selectColumnsPopup.id);
	
	var ul = this.selectColumnsPopup.document.getElementById("dtcol");
	var container = GetElement(this.container_element_id);
	var height = (GetElementHeight(container) - (this.selectColumnsPopup.scope.getBodyWidth() - GetElementHeight(ul)) - 100);
	height = Math.max(height, 100);
	ul.style.height = "auto";
	height = Math.min(height, GetElementHeight(ul));

	ul.style.height = height + "px";
	
	var x = (GetElementWidth(container) - GetElementWidth(el))/2 + GetAbsoluteLeft(container);
	el.style.left = Math.round(x) + "px";

	var y = (GetElementHeight(container) - this.selectColumnsPopup.scope.getBodyHeight())/2 + GetAbsoluteTop(container);
	el.style.top = Math.round(y) + "px";
	
	var dialogBoxContainer = this.selectColumnsPopup.document.getElementById("popup-dialog-container");
	dialogBoxContainer.style.width = dialogBoxContainer.style.height = el.style.height = "auto";
	
	// we grab the parent node because Mozilla doesn't include the borders in the width of the real element
	var dialogBoxContainer = this.selectColumnsPopup.document.getElementById("popup-dialog-container").parentNode;
	el.style.height = this.selectColumnsPopup.scope.GetElementHeight(dialogBoxContainer) + "px";
	el.style.width = (this.selectColumnsPopup.scope.GetElementWidth(dialogBoxContainer)) + "px";
}



DataTable.prototype.CancelSetColumnVisibility = function()
{
	this.selectColumnsPopup.hide();
}

DataTable.prototype.SetColumnVisibility = function(columnsString)
{
	this.selectColumnsPopup.hide();
	
	this.ExtractLayout(columnsString);

	this.SaveLayoutState(columnsString);
		
	var tableDiv = GetElement(this.GetDivName());
	tableDiv.innerHTML = this.CreateTableHTML();

	this.InitializeTable();
	
	this.CompleteTable();

	this._Refresh();
}

DataTable.prototype.ShowColumn = function(columnName)
{
	this.SetColumnVisibility(columnName, true);
}

DataTable.prototype.HideColumn = function(columnName)
{
	this.SetColumnVisibility(columnName, false);
}

var activeFilterControlModifier = new ClassModifier("dt-active");

DataTable.prototype.setFilteredStyle = function(element, isFiltered)
{
	if (isFiltered)
	{
		activeFilterControlModifier.add(element);
	}
	else
	{
		activeFilterControlModifier.remove(element);
	}
}

DataTable.prototype.UpdateAllFilterControls = function(resizeWidth)
{
	if (this.show_filters)
	{
		for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++)
		{
			var sourceColNumber = this.visibleColumnIndex[viewColNumber];
			var column = this.Columns[sourceColNumber];
		
			// mark it as changed
			this.filterInitialized[viewColNumber] = false;
			
			if (column.filter_type == "value")
			{
				var select = GetElement(this.GetFilterControlName(viewColNumber));
				this.setFilteredStyle(select, this.isColumnFiltered(column.name));
				if (resizeWidth)
				{
					select.style.width = "auto";
					var maxitem = this.Columns[sourceColNumber].max_display_value;
					select.options[select.length] = new Option(maxitem, maxitem, false, false);
					// now fix the new width
					select.style.width = select.offsetWidth + "px";
					select.length--;
				}			
			}
			else if (column.filter_type == "toggle")
			{
				var isFiltered = this.isColumnFiltered(column.name);
				var l = GetElement(this.GetFilterControlName(viewColNumber));
				if (!l.INITIALIZED)
				{
					initializeToggleButton(l, isFiltered);
					l.INITIALIZED = true;
				}
				
				if (isFiltered)
				{
					l.title = column.toggle_filter.remove_description;
				}
				else
				{
					l.title = column.toggle_filter.set_description;
				}
			}
		}
	}
	
	var searchBox = document.getElementById(this.GetSearchBoxName());
	if (searchBox)
	{
		this.setFilteredStyle(searchBox, this.HasRowFilter("searchBox"));
	}
};

DataTable.prototype.UpdateFilterSelect = function(viewColNumber)
{	
	startTimer("update", "update select");
	
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	var column = this.Columns[sourceColNumber];
	if (column.filter)
	{
		var select = GetElement(this.GetFilterControlName(viewColNumber));

		var width = select.offsetWidth;

		var pos = (select.selectedIndex === 0 ? 0 : 1);
		// remove all but the first entry
		select.length = 1;
		
		if (!this.isColumnFiltered(column.name))
		{
			var displayedSet = new Array();
			var filterSet = new Array();
			this.dataset.MakeSetFromColumn(sourceColNumber, displayedSet, filterSet);
			
			for (var row = 0; row < displayedSet.length; row++)
			{
				select.options[select.length] = new Option(displayedSet[row], filterSet[row], false, false);
			}
			
			if (pos < select.length)
			{		
				// restore the selected one
				select.selectedIndex = pos;
			}
		}		
		else
		{
			var f = this.GetColumnFilter(column.name);
			var display = f.GetMatchText();
			if (column.max_display_length && display.length > column.max_display_length)
			{
				display = display.slice(0, column.max_display_length-3) + "...";
			}
			
			select.options[select.length] = new Option(display, f.GetMatchText(), false, false);
			select.selectedIndex = 1;
		}		
		
		select.style.width = width + "px";

		this.filterInitialized[viewColNumber] = true;
	}
	
	
	stopTimer("update");
};

// Sorts or resorts the given column.  If the column
// is the same one that is sorted, the order is toggled;
// otherwise it is set to ascending.
DataTable.prototype.ReSortRows = function (viewColNumber) 
{
	// assume the order is true
	var ascending = true;
	
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	if (this.dataset.GetSortedColumn() === sourceColNumber) 
	{ 
		// same column as before, so flip the sort
		ascending = !this.dataset.IsAscending();
	}

	this.SortRows(viewColNumber, ascending);
};

// Sorts the table by the given column, in ascending order if 'ascending' is true
// or descending order.  Redraws the table.
DataTable.prototype.SortRows = function (viewColNumber, ascending) 
{
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	var oldCol = this.dataset.GetSortedColumn();
	this.dataset.SetSortOrder(sourceColNumber, ascending);

	// remove the style from the old column and set it on the new one
	this.UpdateHeaderClasses(viewColNumber);
};
	
	
var nonbreakingSpace = String.fromCharCode(160);

DataTable.prototype._Refresh = function()
{
//	debuggingEnabled = false;
	
	startTimer("refresh", "refresh");
	
	var data = this.dataset.GetViewData(this.current_absolute_row, this.rows_per_page);
	this.viewData = data;

	startTimer("fill", "fill cells");

	for (var relativeViewRow = 0; relativeViewRow < this.rows_per_page && relativeViewRow < data.cells.length; relativeViewRow++) 
	{
		rowID = this.viewData.ids[relativeViewRow];
		
		var row = this.tableDataContainers[relativeViewRow];
		var rowData = this.viewData.cells[relativeViewRow];
		for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++)
		{
			var sourceColNumber = this.visibleColumnIndex[viewColNumber];
			var column = this.Columns[sourceColNumber];

			var cell = row[viewColNumber];
			if (this.editing && column.editable)
			{
				cell.innerHTML = this.GetEditableCellHTML(rowData, rowID, relativeViewRow + this.current_absolute_row, viewColNumber);					
				scanElement(cell);
			}
			else if (column.enumeratedImages)
			{
				// add the icon
				var element = this.GetIcon(column, rowData[sourceColNumber].text);
				element.title = rowData[sourceColNumber].title;
				cell.replaceChild(element, cell.firstChild);
			}
			else
			{
				// add just the text
				var text = rowData[sourceColNumber].display;
				if (is_ie)
				{
					// innerText is IE only, but a lot faster
					cell.innerText = text;
				}
				else
				{
					cell.replaceChild(document.createTextNode(text), cell.firstChild);
				}
				cell.title = rowData[sourceColNumber].title;
			}
		}
		
		if (viewColNumber != 0)
		{
			// only do this if there were some visible columns
			this.ApplyRowStyles(relativeViewRow);		
		}
	}

	for (; relativeViewRow < this.rows_per_page; relativeViewRow++) 
	{
		var row = this.tableDataContainers[relativeViewRow];
		for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++)
		{
			var sourceColNumber = this.visibleColumnIndex[viewColNumber];
			var column = this.Columns[sourceColNumber];

			var cell = row[viewColNumber];
			if (this.editing && column.editable)
			{
				// put in a hidden element so that the vertical height is preserved
				cell.innerHTML = "<input type='text' size='1' disabled style='visibility: hidden'>";
			}
			else if (column.enumeratedImages)
			{
				// get the "no-icon" element so heights and widths are preserved
				var element = this.GetIcon(column, null);
				cell.replaceChild(element, cell.firstChild);
			}
			else
			{
				var text = " ";
				if (is_ie)
				{
					// innerText is IE only, but a lot faster
					cell.innerText = text;
				}
				else
				{
					cell.replaceChild(document.createTextNode(text), cell.firstChild);
				}
			}
		}
		
		this.ApplyRowStyles(relativeViewRow);		
	}


	stopTimer("fill");
	startTimer("extra", "extra");

	this.UpdateScrollBar();
	
	if (this.show_status_bar)
	{
		this.UpdateStatusBar();
	}

	this.SaveState();
	
	stopTimer("extra");
	
//	debuggingEnabled = true;
	stopTimer("refresh");
	
};


var selectedRowModifier = new ClassModifier("dt-row-selected");
var currentRowModifier = new ClassModifier("dt-row-current");
var referenceRowModifier = new ClassModifier("dt-row-reference");
var deletedRowModifier = new ClassModifier("dt-row-deleted");

DataTable.prototype.HighlightSelectedRows = function()
{
	for (var relativeViewRow = 0; relativeViewRow < this.rows_per_page; relativeViewRow++) 
	{
		this.ApplyRowStyles(relativeViewRow);
	}
}

DataTable.prototype.ApplyRowStyles = function(relativeViewRow)
{
	var absoluteRowNumber = this.current_absolute_row + relativeViewRow;

	var selected = current = deleted = reference = false;
	if (absoluteRowNumber < this.dataset.filteredRowCount)
	{
		selected = this.dataset.IsViewRowSelected(absoluteRowNumber);
		current = this.dataset.IsViewRowCurrent(absoluteRowNumber);
		deleted = this.dataset.IsViewRowDeleted(absoluteRowNumber);
		reference = this.dataset.IsViewRowReference(absoluteRowNumber);
	}

	var rowElement = this.tableDataRows[relativeViewRow];
	var className = rowElement.className;
	className = this.ApplyBooleanStyle(selectedRowModifier, className, selected);
	className = this.ApplyBooleanStyle(currentRowModifier, className, current);
	className = this.ApplyBooleanStyle(deletedRowModifier, className, deleted);
	className = this.ApplyBooleanStyle(referenceRowModifier, className, reference);
	
	setClass(rowElement, className);
};

DataTable.prototype.ApplyBooleanStyle = function(modifier, className, value)
{
	if (value)
	{
		return modifier.merge(className);
	}
	else
	{
		return modifier.separate(className);
	}
};

DataTable.prototype.UpdateHeaderClasses = function()
{	
	for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++)
	{
		var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	
		setClass(GetElement(this.GetHeaderName(viewColNumber)), this.GetHeaderStyle(viewColNumber));
	}
};


DataTable.prototype.GetHeaderStyle = function(viewColNumber)
{
	var sourceColNumber = this.visibleColumnIndex[viewColNumber];
	var column = this.Columns[sourceColNumber];
	var classes = [ "dt-title", column.classes ];
	if (column.numeric)
	{
		classes.push("dt-numeric");
	}
	
	if (this.sortable)
	{
		classes.push("dt-sortable");
		if (sourceColNumber == this.dataset.GetSortedColumn())
		{
			if (this.dataset.IsAscending())
			{
				classes.push("dt-ascending");
			}
			else 
			{
				classes.push("dt-descending");
			}
		}
	}
	
	if (column.title_icon)
	{
		classes.push("title-icon");
	}
	
	
	return classes.join(" ");
};

DataTable.prototype.GetDataCellStyle = function(colIndex)
{
	var column = this.Columns[colIndex];
	var classes = [ column.classes ];
	if (column.numeric)
	{
		classes.push("dt-numeric");
	}
	
	return classes.join(" ");
};

DataTable.prototype._SetFilter = function(key, filter)
{
	// for some reason, IE gets confused if this list is
	// unchanged after filtering, even though Mozilla doesn't.
	// So to keep it out of the refresh loop for scrolling,
	// we clear it here.
	this._ResetRowCache();
	this.dataset.SetFilter(key, filter);
	this._RefreshAccumulators();
}

DataTable.prototype._RemoveFilter = function(key)
{
	// see comment in _SetFilter
	this._ResetRowCache();
	this.dataset.RemoveFilter(key);
	this._RefreshAccumulators();
}

// note: delete this function and all references to it
DataTable.prototype._ResetRowCache = function(key)
{
}

DataTable.prototype.isColumnFiltered = function(columnName)
{
	return this.dataset.HasFilter("col-" + columnName);
}

DataTable.prototype.GetColumnFilter = function(columnName)
{
	return this.dataset.GetFilter("col-" + columnName);
}

DataTable.prototype.SetColumnFilter = function(columnName, filter)
{
	this._SetFilter("col-" + columnName, filter);
	this.current_absolute_row = 0;

	this.UpdateAllFilterControls();
	this._Refresh();
};

DataTable.prototype.RemoveColumnFilter = function(columnName)
{
	this._RemoveFilter("col-" + columnName);

	this.current_absolute_row = 0;

	this.UpdateAllFilterControls();
	this._Refresh();
};

DataTable.prototype.SetRowFilter = function(key, filter)
{
	this._SetFilter("row" + key, filter);

	this.current_absolute_row = 0;

	this.UpdateAllFilterControls();
	this._Refresh();
};

DataTable.prototype.RemoveRowFilter = function(key)
{
	this._RemoveFilter("row" + key);

	this.current_absolute_row = 0;

	this.UpdateAllFilterControls();
	this._Refresh();
};

DataTable.prototype.GetRowFilter = function(key)
{
	return this.dataset.GetFilter("row" + key);
}

DataTable.prototype.HasRowFilter = function(key)
{
	return this.dataset.HasFilter("row" + key);
}

DataTable.prototype.ShowPage = function(page)
{
	this.current_page = page;
	this._Refresh();
};


// TODO: fix this function!
DataTable.prototype.SetCurrentRow = function(currentRowID, moveInView)
{
	var viewRow = this.dataset.FindViewRowForID(currentRowID);
	if (viewRow)
	{
		this.dataset.MakeViewRowCurrent(viewRow);
		if (moveInView)
		{
			this.ShowRow(viewRow);
		}		
	}
	else
	{
		this.dataset.RemoveCurrent();
	}
};

DataTable.prototype.ShowRow = function(absoluteRow)
{
	var maxRow = Math.max(this.dataset.FilteredSize() - this.rows_per_page, 0);
	absoluteRow = clamp(absoluteRow, 0, maxRow);
	
	if (this.current_absolute_row !== absoluteRow)
	{
		this.current_absolute_row = absoluteRow;
		this._Refresh();
	}
};

DataTable.prototype.DetermineNewRow = function(delta)
{
	var maxRow = Math.max(this.dataset.FilteredSize() - this.rows_per_page, 0);
	
	var newRow = this.current_absolute_row + delta;
	return clamp(newRow, 0, maxRow);
}	

DataTable.prototype.Move = function(delta)
{
	var newRow = this.DetermineNewRow(delta);
	if (newRow !== this.current_absolute_row)
	{
		this.ShowRow(newRow);
	}		
}	

DataTable.prototype.CanMove = function(delta)
{
	var newRow = this.DetermineNewRow(delta);
	return (newRow != this.current_absolute_row);
}	

DataTable.prototype.SelectRowExclusive = function(newRelativeRow)
{
	var absoluteRowNumber = this.current_absolute_row + newRelativeRow;
	if (absoluteRowNumber < this.dataset.FilteredSize())
	{
		this.dataset.SelectExclusive(absoluteRowNumber);

		this.dataset.MakeReference(absoluteRowNumber);
		
		// if the row we are selecting is not the current row, we turn off the "current" state
		// since only the row we are on will be selected, and the "current" state would be on an unselected row
		if (!this.fixed_current_row && !this.dataset.IsViewRowCurrent(absoluteRowNumber))
		{
			this.dataset.RemoveCurrent();
		}
		
		this.HighlightSelectedRows();		
		this.SaveState();
	}
	else
	{
		this.SelectNoRows();
	}
}

DataTable.prototype.SelectRowAdditive = function(relativeRow)
{
	var absoluteRowNumber = this.current_absolute_row + relativeRow;
	if (absoluteRowNumber < this.dataset.FilteredSize())
	{
		if (this.dataset.IsViewRowSelected(absoluteRowNumber))
		{
			this.dataset.UnselectRow(absoluteRowNumber);
			// if the row we are deselecting is the current row, we turn off the "current" state
			if (!this.fixed_current_row && this.dataset.IsViewRowCurrent(absoluteRowNumber))
			{
				this.dataset.RemoveCurrent();
			}
		}
		else
		{
			this.dataset.SelectAdditive(absoluteRowNumber);
		}
			
		this.dataset.MakeReference(absoluteRowNumber);
		
		this.HighlightSelectedRows();
		this.SaveState();
	}
}

DataTable.prototype.SelectIntermediateRows = function(relativeRow, exclusive)
{
	var absoluteRowNumber = this.current_absolute_row + relativeRow;
	if (absoluteRowNumber < this.dataset.FilteredSize())
	{
		this.dataset.SelectIntermediate(absoluteRowNumber, exclusive, !this.fixed_current_row);
				
		this.HighlightSelectedRows();
		this.SaveState();
	}
}


DataTable.prototype.SelectAllRows = function()
{
	this.dataset.SelectAll();
	this.HighlightSelectedRows();
	this.SaveState();
}

DataTable.prototype.SelectNoRows = function()
{
	this.dataset.ClearSelections();
	if (!this.fixed_current_row)
	{
		this.dataset.RemoveCurrent();
	}
	this.HighlightSelectedRows();
	this.SaveState();
}


DataTable.prototype.ActivateRow = function(relativeRow)
{
	var absoluteRowNumber = this.current_absolute_row + relativeRow;

	if (absoluteRowNumber < this.dataset.FilteredSize())
	{
		if (this.row_activate)
		{
			this.SaveSelection();
			
			var parameter = this.dataset.GetViewRowParameter(absoluteRowNumber);

			var newPage = this.row_activate(mergeParameters(this.base_path, parameter));
						
			// change the cursor to a wait symbol if we're loading a new page
			if (newPage)
			{
				addStyle(document.documentElement, "busy-wait");
			}
		}
	}
}

DataTable.prototype.SetRowCount = function(row_count)
{
	row_count = Math.min(row_count, this.MAXIMUM_ROWS);
	row_count = Math.max(row_count, 3);
	
	var resize = (row_count != this.rows_per_page);
	
	this.rows_per_page = row_count;

	var displayValue = "table-row";
	if (is_ie)
	{
		// IE doesn't like 'table-row', but 'block' seems to work OK
		displayValue = "block";
	}

	// turn on the first rows
	for (var tr_number = 0; tr_number < this.rows_per_page; tr_number++) 
	{
		var row = this.tableDataRows[tr_number];
		row.style.display = displayValue;
	}

	// turn off the remaining rows
	for (var tr_number = this.rows_per_page; tr_number < this.tableDataRows.length; tr_number++) 
	{
		var row = this.tableDataRows[tr_number];
		row.style.display = "none";
	}

	if (resize)
	{
		// mozilla seems to need this resize here to properly update the table
		this.ResizeScrollbar();
		
		this._Refresh();		
	}
}

DataTable.prototype.MakeEditable = function()
{
	this.dataset.MakeEditable();
	this._ResetRowCache();
	for (var n = 0, len = this.tableDataRows.length; n < len; n++)
	{
		var row = this.tableDataRows[n];
		row.ondblclick = null;
	}
	
	// find the containing form element
	var el = this.tableElement;
	for ( ; el.nodeName != "FORM" && el.parentNode; el = el.parentNode) ;
	
	this.formElement = el;
	this.addedRowCount = 0;
	
	this.editing = true;
	this.doneActions = [];
	this._Refresh();
	
	
	debugLog("done");
};

DataTable.prototype.AddNewRow = function()
{
	// TODO: this assumes a specific column format!
	var data = [ "A-", "Center #" + (this.dataset.TotalSize()+1), "", "", 0 ];
	var rowID =	"--addedrow" + this.addedRowCount
	var rowNumber = this.dataset.AddEditRow(data, rowID);	
	this.addedRowCount++;

	this.dataset.SetEditValue(rowNumber, 0, "A-");
	this.doneActions.push(new DatatableAddAction(this));
	this.ShowRow(rowNumber);
	
	this.dataset.SelectExclusive(rowNumber);
	this.dataset.RemoveReference();
	
	this.HighlightSelectedRows();	
	this._ResetRowCache();
	this._Refresh();

	var firstElementName = this.Columns[1].name + "(" + rowNumber + ")";
	this.formElement[firstElementName].focus();
	this.formElement[firstElementName].select();
}



DataTable.prototype.DeleteSelectedRows = function()
{
	this.ToggleDeletedStatus(this.dataset.GetSelectedViewRows(), true, "D");
}

DataTable.prototype.UndeleteSelectedRows = function()
{
	this.ToggleDeletedStatus(this.dataset.GetSelectedViewRows(), false, "-");
}

DataTable.prototype.ToggleDeletedStatus = function(affectedViewRows, deleted, status)
{
	this.dataset.SetDeletedStatus(affectedViewRows, deleted);
	for (var n = 0; n < affectedViewRows.length; n++)
	{	
		var rowNumber = affectedViewRows[n];
		var text = this.dataset.GetEditValue(rowNumber, 0);
		
		this.dataset.SetEditValue(rowNumber, 0, text.substr(0, 1) + status);
	}

	this._ResetRowCache();
	this.doneActions.push(new DatatableDeleteAction(this, affectedViewRows, deleted));
	this._Refresh();
}

DataTable.prototype.SelectionIncludesDeleted = function(isDeleted)
{
	var selected = this.dataset.GetSelectedViewRows();
	for (var n = 0; n < selected.length; n++)
	{
		if (this.dataset.IsViewRowDeleted(selected[n]) === isDeleted)
		{
			return true;
		}
	}
	
	return false;
}


DataTable.prototype.ValueChanged = function(input, rowNumber, colNumber)
{
	if (this.previousInputValue != null)
	{
		this.doneActions.push(new DatatableModifyAction(this, this.previousInputValue, rowNumber, colNumber));
		this.previousInputValue = null;
		this.UpdateMenu();	
	}

	// write this data back to the table
	this.dataset.SetEditValue(rowNumber, colNumber, input.value);
	// and update the icon if needed
	var status = this.dataset.GetEditValue(rowNumber, 0);
	if (status.charAt(0) != "A")
	{
		this.dataset.SetEditValue(rowNumber, 0, "M-");
	}	

	this._RefreshEditRow(rowNumber);
}

DataTable.prototype.SaveOriginalInputValue = function(input, rowNumber, colNumber)
{
	this.previousInputValue = input.value;
}

DataTable.prototype.ForgetOriginalInputValue = function()
{
	this.previousInputValue = null;
}

DataTable.prototype._RefreshEditRow = function(rowNumber)
{
	var relativeRowNumber = rowNumber - this.current_absolute_row;
	if (relativeRowNumber >= 0 && relativeRowNumber < this.dataset.FilteredSize())
	{
		this.formElement["status(" + rowNumber + ")"].value = this.dataset.GetEditValue(rowNumber, 0);
	
		var rowData = this.viewData.cells[relativeRowNumber];
		var cells = this.tableDataRows[relativeRowNumber].cells;
		
		for (var viewColNumber = 0; viewColNumber < this.visibleColumnIndex.length; viewColNumber++)
		{
			var sourceColNumber = this.visibleColumnIndex[viewColNumber];
		
			var container = cells[viewColNumber].firstChild;
			if (this.Columns[sourceColNumber].enumeratedImages)
			{
				var text = this.dataset.GetEditValue(rowNumber, 0);
				var element = this.GetIcon(this.Columns[sourceColNumber], text);
				element.title = rowData[sourceColNumber].title;
				container.replaceChild(element, container.firstChild);
			}
		}	
	}
};

	
DataTable.prototype.Undo = function()
{
	if (this.doneActions.length > 0)
	{
		this.doneActions.pop().Reverse();
		this._Refresh();
	}
}

function DatatableDeleteAction(table, affectedRows, deleted)
{
	this.affectedRows = cloneArray(affectedRows);
	this.deleted = deleted;
	this.CachedSelectList = table.dataset.GetSelectedViewRows();
	this.table = table;
}

DatatableDeleteAction.prototype.Reverse = function()
{
	var newStatus = this.deleted ? "-" : "D";
	for (var n = 0; n < this.affectedRows.length; n++)
	{	
		var rowNumber = this.affectedRows[n];
		var val = this.table.dataset.GetEditValue(rowNumber, 0);
		this.table.dataset.SetEditValue(rowNumber, 0, val.substr(0, 1) + newStatus);
	}
	this.table.dataset.SetDeletedStatus(this.affectedRows, !this.deleted);
	this.table.dataset.SelectGiven(this.CachedSelectList);
	
	this.table._ResetRowCache();
}

function DatatableAddAction(table)
{
	this.CachedSelectList = table.dataset.GetSelectedViewRows();
	this.table = table;
}

DatatableAddAction.prototype.Reverse = function()
{
	this.table.dataset.RemoveLastEditRow();
	this.table.dataset.SelectGiven(this.CachedSelectList);
	
	this.table._ResetRowCache();
}

function DatatableModifyAction(table, oldValue, rowNumber, colNumber)
{
	this.table = table;
	this.oldValue = oldValue;
	this.originalSelectList = table.dataset.GetSelectedViewRows();
	this.oldStatus = table.dataset.GetEditValue(rowNumber, 0).charAt(0);
	this.rowNumber = rowNumber;
	this.colNumber = colNumber;
}

DatatableModifyAction.prototype.Reverse = function()
{
	var val = this.table.dataset.GetEditValue(this.rowNumber, 0);
	this.table.dataset.SetEditValue(this.rowNumber, 0, this.oldStatus + "-");

	this.table.dataset.SetEditValue(this.rowNumber, this.colNumber, this.oldValue);

	this.table.dataset.SelectGiven(this.originalSelectList);
	this.table._ResetRowCache();
}



function mergeParameters(path, parameters)
{
	var result = path;
	if (path.indexOf("?") == -1)
	{
		result += "?";
	}
	else
	{
		result += "&";
	}
	
	result += parameters;
	return result;
}


/*vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv functions for links vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv */


// flips the current sort order if the column is the same as the previous,
// then sorts the table.
function dtSortRows(table, column)
{
	table.ReSortRows(column);

	table._Refresh();
}

function dtSelectAll(table)
{
	table.SelectAllRows();
}

function dtSelectNone(table)
{
	table.SelectNoRows();
}

function dtEdit(table)
{
	table.MakeEditable();
}

function dtSetColumnMatchFilter(table, columnName, match)
{
	table.SetColumnFilter(columnName, new MatchFilter(columnName, match));
}

function dtRemoveColumnFilter(table, columnName)
{	
	table.RemoveColumnFilter(columnName);
}

function dtIsColumnFilterEnabled(table, columnName)
{
	return table.isColumnFiltered(columnName);
}

function dtSetRowFilter(table, key, match)
{
	table.SetRowFilter(key, new RowMatchFilter(match));
}

function dtRemoveRowFilter(table, key)
{	
	table.RemoveRowFilter(key);
}

function dtSetRowCount(table, rowCount)
{
	table.SetRowCount(rowCount);
}

// sets the current row to be the given row ID (assuming it exists)
// moveInView: if given, indicates that the current row should be visible
function dtSetCurrentRow(table, currentRowId, moveInView)
{
	table.SetCurrentRow(currentRowId, moveInView);
}

function dtRemoveCurrentRow(table)
{
	table.dataset.RemoveCurrent();
}

var activeButtons = new Array();

// makes an element have a repeating function that calls the given function
// repeatedly while the mouse button is held down.  if you call this function,
// do not set onClick, onMouseDown, onMouseUp, onDblClick, etc.
function makeRepeating(id, callback, activeCallback)
{
	var running = false;
	var clickDown = false;
	var firstClick = true;
	
	var initialClick = function(e) 
	{
		running = true;
		clickDown = true;
		firstClick = true;
		
		activeCallback(true);
		
		// set this as a timeout so the browser will exit this function, 
		// update for any new style changes in activeCallback, then run the callback
		window.setTimeout(click, 1);
		
		return false;
	}

	var scheduleRepeat = function() 
	{
		// call the click function as fast as possible.
		// The real repeat delay has already occurred.
		window.setTimeout(click, 1);
	}


	var resume = function() 
	{
		if (clickDown)
		{
			running = true;
			
			activeCallback(true);
			
			// set this as a timeout so the browser will exit this function, 
			// update for any new style changes in activeCallback, then run the callback
			window.setTimeout(click, 1);
		}
	}

	var click = function() 
	{
		// skip if the user has released the button in the interim
		if (!running)
		{
			return;
		}		
		
		var delay = 0;			
		if (firstClick)
		{
			// set a longer time for the first time
			delay += 200;
			firstClick = false;
		}

		// This timer calls a function that sets the repeat, so that we know 
		// all other events have been processed before we reschedule this function.
		// Suppose we did not do this, and the callback function takes longer than the delay.
		// Imagine that we schedule the repeat, run the callback function,
		// and the user releases the button while the callback
		// is executing but AFTER the repeat event has been added to the queue.  In that
		// case, we'd run the callback before the mouseUp event, and wouldn't know
		// it had happened.  This way, scheduleRepeat runs before the onMouseUp, 
		// but it only schedules this function, which then runs AFTER the onMouseUp
		// and knows it should not execute.	 It's a lot of work for a small detail.
		window.setTimeout(scheduleRepeat, delay);

		// Note that we schedule the timeout before the callback, so that if the callback function takes longer
		// than the delay, we end up executing them back to back.  In other words,
		// the repeat rate is 1/max(callback_time, delay)--never faster than 1/delay,
		// but never slower than 1/callback_time, either.	
		callback();		
	}

	var disable = function()
	{
		running = false;
		clickDown = false;
		activeCallback(false);
	}

	var pause = function()
	{
		running = false;
		activeCallback(false);
	}
	

	var el = document.getElementById(id);
	el.onmousedown = initialClick;

	el.onmouseup = disable;
	el.onmouseout = pause;
	el.onmouseover = resume;
	el.onclick = stopPropagation;
	
	if (is_ie)
	{
		// need this for IE so the user can drag the cursor off the button
		el.ondragstart = returnFalse;
	}
	
	activeButtons[activeButtons.length] = disable;
	
	// break the closure on element
	el = null;
}

var disableAllRepeatingButtons = function()
{
	for (var i = 0; i < activeButtons.length; i++)
	{
		activeButtons[i]();
	}
}
	

addEvent(document, "onmouseup", disableAllRepeatingButtons);
	

function ScrollButton(table, delta, label, alt)
{
	if (table)
	{
		this.table = table;
		this.table.buttons[this.table.buttons.length] = this;
		this.alt = alt;
		this.styleClass = "scroll-" + label;
		this.id = table.instance_name + "-scroll-" + label;
		this.active = false;
		this.currentStyle = this.styleClass;
		this.delta = delta;
	}
}


ScrollButton.prototype.release = function()
{
	this.table = null;
	this.el = null;
}

ScrollButton.prototype.getHtml = function()
{	
	var id = "scroll-" + this.label;
	
	var html = "";
	html += "<a";
	html += " class='" + this.styleClass + "'";
	html += " id='" + this.id + "'";
	if (this.alt)
	{
		html += " alt='" + this.alt + "'";
		html += " title='" + this.alt + "'";
	}
	html += "></a>";

	return html;	
}		


ScrollButton.prototype.initializeEvents = function()
{
	var me = this;
	function activeCallback(isActive)
	{
		me.active = isActive;
		me.updateState();
	}
	
	function scrollEvent()
	{
		me.table.Move(me.delta);
	}

	makeRepeating(this.id, scrollEvent, activeCallback);
	this.el = document.getElementById(this.id);
}

ScrollButton.prototype.isValid = function()
{
	return this.table.CanMove(this.delta);
}		

ScrollButton.prototype.updateState = function()
{
	var newStyle = null;
		
	if (this.isValid())
	{
		if (this.active)
		{
			newStyle = this.styleClass + "-active";
		}
		else
		{
			newStyle = this.styleClass;
		}
	}
	else if (!this.isValid())
	{
		newStyle = this.styleClass + "-disabled";
	}
		
	// only set the class if it has changed
	if (newStyle !== this.currentStyle)
	{
		this.el.className = (this.el.className.replace(this.currentStyle, newStyle));		
		this.currentStyle = newStyle;
	}
}


var scrollingDown = false;
function ScrollElevator(table)
{
	this.id = table.instance_name + "-elevator";
	this.containerId = table.instance_name + "-elevator-container";
	this.table = table;
	table.elevator = this;
	this.grabOffset = 0;
	this.top = 0;
	this.dragging = false;
	this.draggable = true;
	
	var me = this;
	this.showRow = function ()
	{
		var positionFraction = me.top / me.availableHeight;
		positionFraction = clamp(positionFraction, 0, 1);
		var row = Math.round(positionFraction * (me.table.dataset.FilteredSize() - me.table.rows_per_page));
	
		me.table.ShowRow(row);	
	}	
}

ScrollElevator.prototype.release = function(y)
{
	this.table.elevator = null;
	this.el = null;
	this.scrollbar = null;
	this.elevatorContainer = null;
}

ScrollElevator.prototype.getHtml = function()
{
	return "<div class='dt-elevator-container' id='" + this.containerId + "'><div class='dt-elevator' id='" + this.id + "'><div class='dt-elevator-bottom'><div class='dt-elevator-mid'></div></div></div></div>";
}

ScrollElevator.prototype.initializeEvents = function()
{
	this.el = document.getElementById(this.id);
	this.scrollbar = document.getElementById(this.table.GetScrollBarName());
	this.elevatorContainer = document.getElementById(this.containerId);
	
	var me = this;
	
	// note: these functions may be causing closure memory leaks!
	function scrollMove(e)
	{
		stopPropagation(e);
		me.setTop(getMouseY(e));
	};
	
	function mouseGrab(e)
	{
		if (me.mouseGrab(getMouseY(e)))
		{
			document.onmousemove = scrollMove;
			document.onmouseup = mouseRelease;	
		}

		// disable selecting for IE
		if (is_ie)
		{
			document.onselectstart = returnFalse;
		}
	}
	
	function mouseRelease()
	{
		document.onmousemove = null;
		document.onmouseup = null;
		if (is_ie)
		{
			document.onselectstart = null;
		}
		
		me.mouseRelease();
	}
	
	function page(e)
	{
		me.page(getMouseY(e));
	}
	
	this.el.onmousedown = mouseGrab;
	
//	this.el.onmouseover = notify;
	
//	this.elevatorContainer.onmousedown = page;
	
//	makeRepeating(this.elevatorContainer, page, returnTrue);
}



ScrollElevator.prototype.resize = function()
{
	if (this.scrollbar)
	{
		var topButtons = document.getElementById(this.table.GetTopScrollButtonsName());
		var bottomButtons = document.getElementById(this.table.GetBottomScrollButtonsName());
		var elevatorContainer = document.getElementById(this.containerId);
	
		this.containerHeight = parseInt(this.scrollbar.style.height) - GetElementHeight(topButtons) - GetElementHeight(bottomButtons);
		
		this.elevatorContainer.style.height = this.containerHeight + "px";
		
		// absolute top is the position of the top of the scrollbar (just below the top buttons) in document coordinates
		this.absoluteTop = findPosY(this.elevatorContainer);
		
		this.absoluteBottom = this.absoluteTop + this.containerHeight;

		this.updateState();
	}
}


ScrollElevator.prototype.updateState = function()
{
	if (this.containerHeight && !this.dragging)
	{
		this.draggable = this.table.dataset.FilteredSize() > this.table.rows_per_page;
		
		// compute how big the elevator should be
		if (this.draggable)
		{
			var visibleFraction = this.table.rows_per_page / (this.table.dataset.FilteredSize());
			visibleFraction = Math.min(visibleFraction, 1);
			this.elevatorHeight = Math.round(this.containerHeight * visibleFraction);
			// use a minimum size
			this.elevatorHeight = Math.max(this.elevatorHeight, 20);		
		}
		else
		{
			this.elevatorHeight = this.containerHeight;
		}
		
		this.el.style.height = this.elevatorHeight + "px";
		var children = this.el.getElementsByTagName("div");
		
		var height = this.elevatorHeight;
		for (var n = 0; n < children.length; n++)
		{
			height -= computeVerticalSpacing(children[n]);
			children[n].style.height = height + "px";
		}
		
		// subtract the height of the elevator so that the top position goes from 
		// zero to containerHeight-elevatorHeight, and therefore the bottom
		// goes from elevatorHeight to containerHeight.  Otherwise the elevator
		// exceeds the scrollbar either above or below.
		this.availableHeight = this.containerHeight - this.elevatorHeight;

		// now compute where it should be (which uses its height)
		if (this.draggable)
		{
			var positionFraction = this.table.current_absolute_row / (this.table.dataset.FilteredSize() - this.table.rows_per_page);
			positionFraction = clamp(positionFraction, 0, 1);
			
			this.top = Math.round(positionFraction * this.availableHeight);
		}		
		else
		{
			this.top = 0;
		}
		
		this.el.style.top = this.top + "px";
	}	
}

function clamp(val, low, high)
{
	return Math.max(Math.min(val, high), low);
}

ScrollElevator.prototype.setTop = function(y)
{
	var position = (y - this.absoluteTop) - this.grabOffset;
	position = clamp(position, 0, this.containerHeight - this.elevatorHeight);
	
	this.top = position;
	this.el.style.top = this.top + "px";	
	
	scheduleDelayedEvent(this.showRow, 1, false);
}

ScrollElevator.prototype.mouseGrab = function(y)
{
	if (this.draggable)
	{
		// the offset of the mouse pointer relative to the top of the elevator
		this.grabOffset = (y - this.absoluteTop) - this.top;
		
		this.dragging = true;
		addStyle(this.el, "dt-elevator-drag");
	}
	
	return this.draggable;
}

ScrollElevator.prototype.mouseRelease = function(y)
{
	this.dragging = false;
	removeStyle(this.el, "dt-elevator-drag");
}

ScrollElevator.prototype.page = function(y)
{
	if (this.draggable)
	{
		// the offset of the mouse pointer relative to the top of the elevator
		var clickOffset = (y - this.absoluteTop) - this.top;

		if (clickOffset < 0)
		{
			this.table.Move(-this.table.rows_per_page);
		}
		else if (clickOffset > this.elevatorHeight)
		{
			this.table.Move(this.table.rows_per_page);
		}
	}
}

