
/**
 *
 * ListControl - singleton
 *
 * Functions for controlling pgList divs & containers
 *
 */
var ListControl = new function()
{
	var oListDiv;
	var oNavBarDiv;
	var oNavBarSpacer;
	var oSmallColumnContainer;
	var oSmallLoadingDiv;
	var oFilterDescTable;
	var oFilterMenuContainer;
	var oRefinePopupContainer;
	var sTitlePrototype; 	// prototype of page title w/ empty spot for filter name
	var sFilterName;
	var oFilterRefineAjax;

	var iScrollStateXOrig = 0;
	var iScrollStateYOrig = 0;
	
	var POPUP_PADDING = 7;
	var FILTER_TAB_PADDING = 8;
	
	this.init = function()
	{
		oRefinePopupContainer = elById('containerFilterOpt');
		ListControl.sendFilterRefineRequest();
		oListDiv = elById("pgListContainer");
		oNavBarDiv = elById("navBarContainer");
		oNavBarSpacer = elById("navBarSpacer");
		oSmallColumnContainer = elById("smallColumnContainer");
		oSmallLoadingDiv = elById("smallLoadingDiv");
		oFilterDescTable = elById("idFilterDescription");
		oFilterMenuContainer = elById("filterMenuContainer");
		
		if (oSmallColumnContainer && oSmallLoadingDiv)
		{
			var pxTop = calculateOffset(oSmallColumnContainer, "offsetTop");
			oSmallLoadingDiv.style.top = pxTop + "px";
			oSmallLoadingDiv.style.left = calculateOffset(oSmallColumnContainer, "offsetLeft") + "px";
			if (window.opera) oSmallLoadingDiv.style.background = "url(images/transparent.png) left bottom repeat";
		}
	}

	this.storeOriginalScrollState = function()
	{
		if (!document.formWithScrollState) return;
		iScrollStateXOrig = document.formWithScrollState.iScrollStateX.value;
		iScrollStateYOrig = document.formWithScrollState.iScrollStateY.value;
	}

	this.scrollWindow = function(e)
	{
		if (!document.formWithScrollState) return;
		document.formWithScrollState.iScrollStateX.value = scrollLeft();
		document.formWithScrollState.iScrollStateY.value = scrollTop();
	}

	this.restoreWindowScrollState = function()
	{
		if (!document.formWithScrollState) return;
		try{window.scrollTo(iScrollStateXOrig, iScrollStateYOrig);}catch(e){};
	}

	// sendFilterRefineRequest
	//
	// Immediately send request to fill in the filter refine popup data
	//
	this.sendFilterRefineRequest = function()
	{
		if (!oRefinePopupContainer) return; // Don't send request for refine filter popups if no container exists

		if (!oFilterRefineAjax) oFilterRefineAjax = new AjaxRequest();
		if (oFilterRefineAjax.isBusy())
		{
			// request is already busy, wait half a sec and try again
			setTimeout('ListControl.sendFilterRefineRequest()', 500);
		}
		else
		{
			oFilterRefineAjax.initialize();
			oFilterRefineAjax.onreadystatechange(this.resultFilterRefineRequest);
			oFilterRefineAjax.open("GET", sDefaultURI + "?fAlaCarte=1&pg=pgAlaCarteRefineFilter", true);
			oFilterRefineAjax.send(null);
		}
	}

	this.resultFilterRefineRequest = function()
	{
		if (!oFilterRefineAjax || !oFilterRefineAjax.isReady()) return;
		var oLoadingPopup = elById('idFilterOptLoading');
		if (oLoadingPopup && isVisible(oLoadingPopup))
		{
			theMgr.hideAllPopups();
		}
		var sHTML = XMLParser.getCDataFrom(oFilterRefineAjax.responseXML(), "sHTML");
		if (oRefinePopupContainer && sHTML && sHTML.length)
			oRefinePopupContainer.innerHTML = sHTML;

		var ix;
		// All of the select id's in the filter popups need to remain visible when popups appear
		var rgSelectId = XMLParser.getNodeArrayFrom(oRefinePopupContainer, "select");
		for (ix = 0; ix < rgSelectId.length; ix++)
		{
			if (rgSelectId[ix].id) theMgr.addNoHide(rgSelectId[ix].id);
		}

		// All of the popups themselves need to go invisible when a single popup appears
		var rgPopupId = XMLParser.getNodeArrayFrom(oFilterRefineAjax.responseXML(), "idPopup")
		if (!rgPopupId || !rgPopupId.length) return;
		for (ix = 0; ix < rgPopupId.length; ix++)
		{
			var id = XMLParser.getTextOf(rgPopupId[ix]);
			if (id) theMgr.add(id);
		}
	}

	this.titlePrototype = function(sPrototype)
	{
		if (sPrototype && sPrototype.length) sTitlePrototype = sPrototype;
		return sTitlePrototype;
	}

	this.filterName = function(sFilterNameInit)
	{
		if (sFilterNameInit && sFilterNameInit.length) sFilterName = sFilterNameInit;
		return sFilterName;
	}

	this.isListOrGridView = function()
	{
		return (oListDiv != null);
	}

	this.isGridView = function()
	{
		var o = elById("bGridView");
		return o;
	}
	
	this.getListDiv = function()
	{
		return oListDiv;
	}
	
	this.getFilterMenuContainer = function()
	{
		return oFilterMenuContainer;
	}
	
	//  Get right pixel boundary of nav bar (not including spacer)
	this.getNavBarRight = function()
	{
		if (!oNavBarDiv) return 0;
		return calculateOffset(oNavBarDiv, "offsetLeft") + oNavBarDiv.offsetWidth;
	}
	
	// Maximize the width of the nav bar to 100% of visible window
	// Use navbar spacer to cover any remaining scroll width
	this.maximizeNavBar = function()
	{
		if (!oNavBarDiv) return;
		oNavBarDiv.style.width = "100%";
		var oT = XMLParser.getNodeFrom(oNavBarDiv, "table");
		oT.style.width = "100%";
		if (oFilterDescTable)
		{
			oFilterDescTable.style.width = "100%";
		}
		
		GridControl.refreshTableCoords();
		this.extendNavBarSpacer(GridControl.getTableRight());
	}
	
	// Extend the nav bar spacer in case the grid extends beyond nav bar's right boundary
	this.extendNavBarSpacer = function(pxRight)
	{
		if (!oNavBarSpacer) oNavBarSpacer = elById("navBarSpacer");
		if (!oNavBarSpacer) return;
		
		var pxLeft = oNavBarDiv.offsetWidth;
		var pxFilterTable;
		if (oFilterDescTable)	pxFilterTable = oFilterDescTable.offsetWidth;
		var pxWidth = pxRight - pxLeft;
		oNavBarSpacer.style.left = pxLeft + "px";
		if (pxWidth > 0)
		{
			oNavBarSpacer.style.width = pxWidth + "px";
			var oT = XMLParser.getNodeFrom(oNavBarSpacer, "table");
			oT.style.width = pxWidth + "px";
			oNavBarSpacer.style.display = "";
		}
		else
		{
			oNavBarSpacer.style.display = "none";
		}
		oNavBarDiv.style.width = pxLeft + "px";
		if (oFilterDescTable)	oFilterDescTable.style.width = pxFilterTable + "px";
	}
	
	this.showLoadingDiv = function()
	{
		FadeManager.startFadeIn("columnPopup", oSmallLoadingDiv, ListControl.setLoadingDivSize, false);
	}
	
	this.hideLoadingDiv = function()
	{
		FadeManager.startFadeOut("columnPopup", oSmallLoadingDiv, ListControl.setLoadingDivSize);
	}
	
	this.setLoadingDivSize = function()
	{
		if (!oSmallLoadingDiv || !oSmallColumnContainer) return;
		oSmallLoadingDiv.style.height = (oSmallColumnContainer.offsetHeight+POPUP_PADDING) + "px";
		oSmallLoadingDiv.style.width = oSmallColumnContainer.offsetWidth + "px";
	}

	this.sendListRefreshRequest = function()
	{
		if (!oAjaxRequest) oAjaxRequest = new AjaxRequest();
		if (oAjaxRequest.isBusy())
		{
			// request is already busy, wait half a sec and try again
			setTimeout('ListControl.sendListRefreshRequest()', 500);
		}
		else
		{
			oAjaxRequest.initialize();
			oAjaxRequest.onreadystatechange(this.resultListRefreshRequest);
			oAjaxRequest.open("GET", sDefaultURI + "?fAlaCarte=1&pg=pgList", true);
			oAjaxRequest.send(null);
		}
	}

	this.resultListRefreshRequest = function()
	{
		if (!oAjaxRequest || !oAjaxRequest.isReady()) return;
		var sHTML = XMLParser.getCDataFrom(oAjaxRequest.responseXML(), "sHTML");
		var sSignature = XMLParser.getCDataFrom(oAjaxRequest.responseXML(), "sSignature");
		var oContainer = elById("pgListContainer");
		if (sHTML && oContainer)
		{
			moveToHold(oContainer);
			oContainer.innerHTML = sHTML;
			restoreFromHold(oContainer);
			ListControl.restoreWindowScrollState();

			ListControl.overrideSignatures(sSignature);
			initializeListView();
			Info.hide();
		}
		ListControl.addHoverSpansFromXML(oAjaxRequest.responseXML());
	}

	// fixCacheState
	//
	// If this list was loaded via cache and
	// its signature does not match the user's latest list signature,
	// restore the latest list in order to maintain consistency w/
	// server-side list representation
	//
	// Return true iff signature does not match
	// and we use AjaxRequest to reload list
	//
	this.fixCacheState = function()
	{
		if (!this.isSignatureSafe())
		{
			Info.show(Lang.getString("FB_LOADING_LIST_DATA"));
			this.sendListRefreshRequest();
			return true;
		}
		return false;
	}

	// isSignatureSafe
	//
	// Returns true iff the newest list signature matches that of 
	// the originally cached page
	//
	this.isSignatureSafe = function()
	{
		if (!document.formWithSignature || !document.formWithSignature.sListSignatureNew)
			return true;
		var oNew = document.formWithSignature.sListSignatureNew;
		return (oNew && (oNew.value == sListSignatureOld));
	}

	// updateSignature
	//
	// Update the current list signature to reflect the current list state
	//
	this.updateSignature = function(bDirtyFilterIcon)
	{
		var oNew = document.formWithSignature.sListSignatureNew;
		oNew.value = this.getSignature();

		// Update icons if we need to convey filter's dirty status
		// and we're not looking at a search result
		if (bDirtyFilterIcon && !GridControl.isSearchResult())
			Icons.changeIcons(Icons.ENABLED, null, null, null);
	}

	// overrideSignatures
	//
	// Override both signatures w/ supplied sig from server
	//
	this.overrideSignatures = function(sSig)
	{
		if (!sSig || !sSig.length) return;
		var oNew = document.formWithSignature.sListSignatureNew;
		sSig = trim(sSig);
		oNew.value = sSig;
		sListSignatureOld = sSig;
	}

	// getSignature
	//
	// Get the current representation of the list as a string signature
	// (...to be compared against server-side equivalent in order to fix
	// incorrect cached list states)
	//
	this.getSignature = function()
	{
		var sSig = "sFilterName=" + makeNonHTMLEntities(this.filterName());
		if (this.isGridView())
		{
			sSig += "&sColSig=" + GridControl.getColSignature() + "&sSortSig=" + GridControl.getSortSignature();
			if (GridControl.isPrimaryReverseSort()) sSig += "&bPrimaryReverseSort=true";
		}
		return sSig;
	}
	
	// addHoverSpansFromXML
	//
	// Add hoverspans to the DOM from XML
	//
	this.addHoverSpansFromXML = function(oXML)
	{
		var rgHoverSpan = XMLParser.getNodeArrayFrom(oXML, "HoverSpan");
		var sId, oSpan;
		var oSpanContainer = elById("hoverSpanContainer");
		
		for (var ix = 0; ix < rgHoverSpan.length; ix++)
		{
			sId = XMLParser.getTextFrom(rgHoverSpan[ix], "sId");
			oSpan = XMLParser.getNodeUsingIdFrom(oSpanContainer, "span", sId);
			if (!oSpan)
			{
				sHTML = XMLParser.getCDataFrom(rgHoverSpan[ix], "sHTML");
				oSpanContainer.innerHTML += sHTML;
			}
		}
		theHoverTipMgr.reset();
	}
	
} // end ListControl singleton

/**
 *
 * Icons - list view's toolbar icon controller
 *
**/
var Icons = new function()
{

	this.ENABLED = 0;
	this.DISABLED = 1;
	this.CONFIRMED = 2; // disk icon only
	var rgsFileSuffix = ["", "-disabled", "-confirmed"];

	var IconInfo = null;
	var oNote = null;
	var oContents = null;
	var oNoteTarget = null;
	var bNoteImmediate = false;

	this.init = function()
	{
		if (!oNote || !oContents)
		{
			oNote = elById("noteListToolbar");
			if (!oNote) return;
			oContents = XMLParser.getNodeFrom(oNote, "nobr");

			IconInfo = 	{ 
					"imgDiskStatus" : 
						[
						Lang.getString("FB_SAVE_CURRENT_FILTER_AS"),
						Lang.getString("FB_CANNOT_SAVE_SEARCH"),
						function() {return swap1(Lang.getString("FB_CURRENT_FILTER_SAVED_AS"), ListControl.filterName());}
						],
					"imgColumnsStatus" : 
						[
						Lang.getString("FB_ADD_REMOVE_COLUMN"),
						function() {return ListControl.isGridView() ? Lang.getString("FB_NO_COLUMNS_WITHOUT_CASES") : Lang.getString("FB_SWITCH_TO_GRID_FOR_COLUMNS");},
						null
						],
					"imgListStatus" : 
						[
						function() {return ListControl.isGridView() ? Lang.getString("FB_FILTER_SWITCHLISTVIEW") : Lang.getString("FB_FILTER_SWITCHGRIDVIEW");},
						null,
						null
						],
					"imgRssStatus" : 
						[
						Lang.getString("FB_RSS_TITLE"),
						Lang.getString("FB_NO_RSS_WITHOUT_SAVE"),
						null
						]
					};
		}
	}

	this.changeIcons = function(statusDisk, statusCol, statusList, statusRss)
	{
		if (statusDisk != null) this.setSingleIconStatus("Disk", statusDisk);
		if (statusCol != null) this.setSingleIconStatus("Columns", statusCol);
		if (statusList != null) this.setSingleIconStatus("List", statusList);
		if (statusRss != null) this.setSingleIconStatus("Rss", statusRss);
	}

	this.setSingleIconStatus = function(sTitle, status)
	{
		var o = elById('img' + sTitle + 'Status');
		if (!o || !o.parentNode) return;

		o.src = "images/icon-" + sTitle.toLowerCase() + rgsFileSuffix[status] + ".png";
		var bDisabled = (status == this.DISABLED);
		o.className = bDisabled ? "bigiconDisabled" : "bigicon";
		if (sTitle != "Disk")
		{
			o.parentNode.onclick = function() {return !bDisabled;};
			bDisabled ? o.parentNode.setAttribute("keydisabled", "true") : o.parentNode.removeAttribute("keydisabled");
		}
	}

	this.getStatus = function(oIcon)
	{
		if (!oIcon || !oIcon.src) return null;
		for (var iStatus in rgsFileSuffix)
		{
			if (rgsFileSuffix[iStatus].length == 0) continue;
			if (oIcon.src.indexOf(rgsFileSuffix[iStatus]) != -1) return iStatus;
		}
		return this.ENABLED;
	}

	this.showNote = function(oIcon)
	{
		this.init();
		oNoteTarget = oIcon;
		if (bNoteImmediate)
			this.doShowNote(oNoteTarget.id);
		else
			setTimeout("Icons.doShowNote('" + oNoteTarget.id + "')", 750);
	}

	this.doShowNote = function(sId)
	{
		if (!oNoteTarget || oNoteTarget.id != sId || oNote.style.display == "") return;
		bNoteImmediate = true;

		oNote.style.visibility = "hidden";
		oNote.style.display = "";

		var oVal = IconInfo[oNoteTarget.id][Icons.getStatus(oNoteTarget)];
		if (!oVal) return;

		oContents.innerHTML = oVal.call ? oVal.call() : oVal;

		var pxIconTop = calculateOffset(oNoteTarget, "offsetTop");
		var pxIconLeft = calculateOffset(oNoteTarget, "offsetLeft");
		var pxNoteLeft = (pxIconLeft + (oNoteTarget.offsetWidth/2)- (oNote.offsetWidth/2));
		pxNoteLeft = Math.min(pxNoteLeft, (windowRight() - oNote.offsetWidth - 20)); // Don't let note extend beyond window
		oNote.style.left = pxNoteLeft + 'px';
		oNote.style.top = (pxIconTop + oNoteTarget.offsetHeight + 10) + 'px';

		oNote.style.visibility = "visible";
	}

	this.cancelNote = function()
	{
		this.init();
		oNote.style.display = "none";
		oNoteTarget = null;
		setTimeout(this.stopImmediateNote, 750);
	}

	this.stopImmediateNote = function()
	{
		if (oNoteTarget == null) bNoteImmediate = false;
	}
}

/**
 *
 * GridControl - singleton
 *
 * Functions for controlling grid view
 * 
 */
var GridControl = new function()
{
	// Grid data
	var rgGridData;				// array of JS objects for storing row content
	var mapBugToRow;			// map of ixBug to ixRow for reverse lookups
	var mapIdToCol;				// map of element id's to ixColumns so we don't have to change onclicks
	var rgRowNodeCache;			// cache of <tr> nodes to avoid using document.getElementById
	var rgHeaderBarCache;		// cache of <tr> header bar nodes
	var bOverflow;				// true if there is a bug overflow (>200) for the current filter
	var bSearchResult;			// true if current grid is a search result
	var bShallowWalkDone;		// true if each row has been mined for data
	var bDeepWalkDone;			// true if each individual column has been mined for data
	var oColumnInfo;			// span whose attributes hold column info
	
	// AjaxRequest obj's
	var oAjaxGridAdd;			// grid control's ajax request object for adding columns
	var oAjaxGridRemove;		// ajax request obj for removing columns
	var oAjaxGridResize;		//  "    "       "   "  resizing columns
	var oAjaxGridReorder;		// "    "       "   "  reordering columns
	
	// Table info
	var oTable;					// reference to grid row table
	var oParentHolder;			// temporary reference to table's parent
	var oSiblingHolder;			// temporary reference to table's nextSibling
	var oRow;					// temporary row
	var oHeaderPrototype;		// empty prototype of group header
	var oSortArrowPrototype;	// prototype of directional sort arrow
	var pxTableTop;				// top of table (not including first group header)
	var pxTableBottom;			// bottom of table (not including any action buttons or headers)
	var pxTableLeft;			// left of table
	var pxTableRight;			// right of table
	var pxTableWidth;			// width of table
	
	// Column info
	var cCols;
	var rgColInfo;				// stores per-column information such as header string
	var ixColToSort;			// index of column to be sorted
	var rgSortType;				// types of column being sorted
	var bPrimaryReverseSort = false;	// true if primary sort is reversed
	var fDataTypeToSort;		// flag indicating data type of col to sort
								// 0 = int, 1 = string, 2 = date, 3 = reversedate, 4 = date|string
								// "reversedate" = default behavior shows latest date first
								// "date|string" = primary sort is by date, secondary by string
	
	// Dragging columns
	var pxDragThreshold = 5;	// must move mouse this many pixels before drag starts
	var bColDragging = false;	// true if dragging column header in order to rearrange cols
	var bColDragMoved = false;	// true if column dragged passed minimum movement threshold
	var ixColToDrag;			// ix of column header being dragged
	var pxDragOffsetX;			// pixel offset between mouse and left of col header being dragged
	var pxDragOffsetY;
	var pxDragStartX;
	var pxDragStartY;
	var rgHotSpots;
	
	// Resizing columns
	var ixColToResize;			// index of col being resized
	var bResizing = false;		// true if resizing column
	var bResizeMoved = false;	// true if user has changed column size
	var oHeaderToResize;		// node reference for header being resized
	var pxResizeOffset;			// left offset of header being resized
	
	// "Ghost" divs for reorder/resize effects
	var oGhostDiv;				// impersonate column header "ghost" for DnD
	var oGhostLine;				// ghost line for column drop hotspots
	var oGhostDownArrow;		// ghost arrow for column drop hotspots
	var oGhostLeftArrow;		// ghost arrow for column resize movement
	var oGhostRightArrow;		// ghost arrow for column resize movement
	var oGhostArea;				// ghost area for column resize width
	
	var MIN_COL_WIDTH = 15;			// pixel min col width
	var MAX_COL_AUTO_WIDTH = 1000;	// max pixel width for auto column resize
	var GRID_PADDING = 4;			// pixel padding
	
	// init
	//
	// Collect all initial grid data
	//
	this.init = function()
	{
		oTable = elById("bugGrid");
		mapBugToRow = new Array();
		mapIdToCol = new Array();
		rgGridData = new Array();
		rgColInfo = new Array();
		rgRowNodeCache = new Array();
		rgHeaderBarCache = new Array();
		this.collectColumnInfo();
		this.collectSortInfo();
		this.collectOverflowInfo();
		this.collectSearchInfo();
		this.refreshTableCoords();
		this.walkTheGrid(false);
		this.enableColumnPopupLinks();
		this.formatGhostDivs();
		this.createSortArrow(null, null);
	}
	
	this.stopActions = function()
	{
		this.stopColumnDrag();
		this.stopColumnResize();
	}
	
	// collectSortInfo
	//
	// Grab information regarding grid view's initial sort order
	//
	this.collectSortInfo = function()
	{
		rgSortType = new Array();
		if (!oColumnInfo) oColumnInfo = elById("columnAndSortValues");
		if (!oColumnInfo) return;
		
		bPrimaryReverseSort = (oColumnInfo.getAttribute("primaryreverse") != null);
		for (var ixSort = 0; ixSort < 3; ixSort++)
		{
			rgSortType[ixSort] = oColumnInfo.getAttribute("sorttype" + ixSort);
		}
	}

	this.isSorted = function(ixCol)
	{
		return (this.isSortMatch(rgColInfo[ixCol].iType, rgSortType[0]) || 
			this.isSortMatch(rgColInfo[ixCol].iType, rgSortType[1]) || 
			this.isSortMatch(rgColInfo[ixCol].iType, rgSortType[2]));
	}

	this.isPrimarySort = function(ixCol)
	{
		return (this.isSortMatch(rgColInfo[ixCol].iType, rgSortType[0]));
	}
	
	// collectColumnInfo
	//
	// Grab information regarding column headers
	//
	this.collectColumnInfo = function()
	{
		if (!oColumnInfo) oColumnInfo = elById("columnAndSortValues");
		
		rgColInfo = new Array();
		cCols = 0;
		for( var i = 0; true; i++)
		{
			var sType = oColumnInfo.getAttribute("colheadertypestring_" + i);
			var iType = oColumnInfo.getAttribute("colheadertypeint_" + i);
			var sHeader = oColumnInfo.getAttribute("colheadervalue_" + i);
			if (sHeader && sType && iType)
			{
				rgColInfo[i] = new Object();
				rgColInfo[i].sHeader = sHeader;
				rgColInfo[i].sType = sType;
				rgColInfo[i].iType = iType;
				cCols++;
			}
			else break;
		}
	}
	
	// formatGhostDivs
	//
	// Create and format the "ghost" divs, lines, and arrows that
	// appear whenever a column header is dragged or resized
	//
	this.formatGhostDivs = function()
	{	
		oGhostLeftArrow = this.createGhostArrow("leftArrow.gif");
		oGhostRightArrow = this.createGhostArrow("rightArrow.gif");
		oGhostDownArrow = this.createGhostArrow("downArrow.gif");

		oGhostDiv = document.createElement("div");
		this.formatSingleGhostDiv(oGhostDiv);
		oGhostDiv.style.textAlign = "left";
		oGhostDiv.style.padding = "2px 0px 0px 4px";
		oGhostDiv.className = "ghostFont";
		oGhostDiv.style.color = "#fff";
		oGhostDiv.style.background = "#0F4962 url(images/sidebar-h-bg.gif) left bottom repeat-x";
		oGhostDiv.style.zIndex = 2;
		document.body.insertBefore(oGhostDiv, null);
		
		oGhostArea = document.createElement("div");
		this.formatSingleGhostDiv(oGhostArea);
		if (window.opera)	oGhostArea.style.background = "url(images/transparent.png) left bottom repeat";
		else			oGhostArea.style.backgroundColor = "silver";
		document.body.insertBefore(oGhostArea, null);
		
		oGhostLine = document.createElement("div");
		oGhostLine.style.backgroundColor = "silver";
		this.formatSingleGhostDiv(oGhostLine);
		oGhostLine.style.width = "2px";
		document.body.insertBefore(oGhostLine, null);
	}
	
	this.hideGhostDivs = function()
	{
		if (oGhostDiv) oGhostDiv.style.display = "none";
		if (oGhostLine) oGhostLine.style.display = "none";
		if (oGhostDownArrow)
		{
			oGhostDownArrow.style.display = "none";
			oGhostDownArrow.style.visibility = "hidden";
		}
		if (oGhostArea) oGhostArea.style.display = "none";
		if (oGhostLeftArrow) oGhostLeftArrow.style.display = "none";
		if (oGhostRightArrow) oGhostRightArrow.style.display = "none";
	}
	
	this.formatSingleGhostDiv = function(oDiv)
	{
		oDiv.style.display = "none";
		oDiv.style.position = "absolute";
		oDiv.style.opacity = .5;
		oDiv.style.filter = "alpha(opacity=50)";
	}
	
	this.createGhostArrow = function(sImgLoc)
	{
		var oArrow = document.createElement("div");
		this.formatSingleGhostDiv(oArrow);
		oArrow.style.visibility = 'hidden';
		var oImg = document.createElement("img");
		oImg.src = sImgLoc;
		oArrow.insertBefore(oImg, null);
		document.body.insertBefore(oArrow, null);
		return oArrow;
	}
	
	this.enableColumnPopupLinks = function()
	{
		var oNotice = elById("smallLoadingNotice");
		var oContainer = elById("smallColumnContainer");
		
		if (oNotice) oNotice.style.display = "none";
		if (oContainer) oContainer.style.display = "";
		
		var rgBoxes = XMLParser.getNodeArrayFrom(oContainer, "input");
		for (var ix = 0; ix < rgBoxes.length; ix++)
		{
			var bInGrid = false;
			var iType = -1;
			if (rgBoxes[ix].id && rgBoxes[ix].id.indexOf("_") != -1)
				iType = rgBoxes[ix].id.substring(rgBoxes[ix].id.indexOf("_")+1);
			
			for (var ixCol = 0; ixCol < rgColInfo.length; ixCol++)
			{
				if (rgColInfo[ixCol].iType == iType)
				{
					bInGrid = true;
					break;
				}
			}
			this.styleColumnPopupLinkByBox(rgBoxes[ix], bInGrid);
		}
	}
	
	this.styleColumnPopupLinkByType = function(iType, bInGrid)
	{
		var oBox = elById("columnPopupBox_"+iType);
		this.styleColumnPopupLinkByBox(oBox, bInGrid);
	}

	this.styleColumnPopupLinkByBox = function( oBox, bInGrid )
	{
		if (oBox) oBox.checked = bInGrid;
	}
	
	this.addColumnInfo = function(sHeaderInit, sTypeInit, iTypeInit)
	{
		var ix = cCols;
		rgColInfo[ix] = new Object();
		rgColInfo[ix].sHeader = sHeaderInit;
		rgColInfo[ix].sType = sTypeInit;
		rgColInfo[ix].iType = iTypeInit;
		cCols++;
	}
	
	this.removeColumnInfo = function(ixColumn)
	{
		for (var ix = ixColumn; ix < (rgColInfo.length-1); ix++)
		{
			rgColInfo[ix] = rgColInfo[ix+1];
		}
		rgColInfo[rgColInfo.length-1] = null;
		cCols--;
		
		if (bDeepWalkDone)
		{
			for (var iRow = 0; iRow < rgGridData.length; iRow++)
			{
				for (ix = ixColumn; ix < (rgGridData[iRow].length-1); ix++)
				{
					rgGridData[iRow][ix] = rgGridData[iRow][ix+1];
				}
				rgGridData[iRow][rgGridData[iRow].length-1] = null;
			}
		}
	}
	
	this.moveColumnInfo = function(ixColToMove, ixColTarget)
	{
		if (ixColToMove == ixColTarget || ixColToMove == (ixColTarget-1)) return;
		if (ixColTarget > ixColToMove) ixColTarget--;
		var oInfoTemp = rgColInfo[ixColTarget];
		rgColInfo[ixColTarget] = rgColInfo[ixColToMove];
		
		var ix;
		if (ixColTarget > ixColToMove)
		{
			for (ix = ixColToMove; ix < ixColTarget; ix++)
			{
				if (ix < (rgColInfo.length-1))
					rgColInfo[ix] = rgColInfo[ix+1];
			}
			rgColInfo[ixColTarget-1] = oInfoTemp;
		}
		else
		{
			for (ix = ixColToMove; ix > ixColTarget; ix--)
			{
				if (ix > 0)
					rgColInfo[ix] = rgColInfo[ix-1];
			}
			rgColInfo[ixColTarget+1] = oInfoTemp;
		}
	}
	
	this.moveGridData = function(ixColToMove, ixColTarget)
	{
		if (!bDeepWalkDone) return;
		if (ixColToMove == ixColTarget || ixColToMove == (ixColTarget-1)) return;
		if (ixColTarget > ixColToMove) ixColTarget--;
		
		var max = rgGridData.length;
		for (var ixRow = 0; ixRow < max; ixRow++)
		{
			var oInfoTemp = rgGridData[ixRow][ixColTarget];
			rgGridData[ixRow][ixColTarget] = rgGridData[ixRow][ixColToMove];
			
			var ix;
			if (ixColTarget > ixColToMove)
			{
				for (ix = ixColToMove; ix < ixColTarget; ix++)
				{
					if (ix < (rgGridData[ixRow].length-1))
						rgGridData[ixRow][ix] = rgGridData[ixRow][ix+1];
				}
				rgGridData[ixRow][ixColTarget-1] = oInfoTemp;
			}
			else
			{
				for (ix = ixColToMove; ix > ixColTarget; ix--)
				{
					if (ix > 0)
						rgGridData[ixRow][ix] = rgGridData[ixRow][ix-1];
				}
				rgGridData[ixRow][ixColTarget+1] = oInfoTemp;
			}
		}
	}
	
	// collectOverflowInfo
	//
	// Grab information regarding any bug overflow
	// for the current filter
	//
	this.collectOverflowInfo = function()
	{
		if (XMLParser.getCustomTagValueFrom(document, "fb:overflow"))
			bOverflow = true;
		else
			bOverflow = false;
	}
	
	// collectSearchInfo
	//
	// Grab information regarding whether or not
	// current grid is a search result
	//
	this.collectSearchInfo = function()
	{
		if (XMLParser.getCustomTagValueFrom(document, "fb:searchresult"))
			bSearchResult = true;
		else
			bSearchResult = false;
	}
	
	// walkTheGrid
	//
	// Traverse over the grid view's grid and collect
	// data from fb:cellvalue tags for JS array
	//
	this.walkTheGrid = function( bDeepWalk )
	{
		if (!mapBugToRow) return;
		if (!oTable) return;
		
		if (bShallowWalkDone && bDeepWalk)
		{
			this.startDeepWalker();
		}
		else
		{			
			bShallowWalkDone = false;
			rgGridData = new Array();
			this.startShallowWalker(0, 100, bDeepWalk);
		}
	}
	
	this.startShallowWalker = function(ixBugRowStart, nStep, bDeepWalkAfter)
	{
		var cBugRows = ixBugRowStart;
		var rgTRs = oTable.getElementsByTagName("tr");
		var max = rgTRs.length;
		var ix;
		for (ix = cBugRows; (ix < max) && (ix < (ixBugRowStart+nStep)); ix++)
		{
			oRow = rgTRs[ix];
			var ixBug = oRow.getAttribute("ix");
			if (ixBug && (oRow.id == ("row_"+cBugRows)))
			{
				rgRowNodeCache[cBugRows] = oRow;
				rgGridData[cBugRows] = new Array();
				rgGridData[cBugRows].ixBug = ixBug;
				// The following (seemingly redundant) data is stored
				// b/c the object needs to reference its own index in the array
				// in order to keep our sorts stable
				rgGridData[cBugRows].ixRow = cBugRows;
				
				this.setBugToRow(ixBug, cBugRows);

				cBugRows++;
			}
		}
		
		if (ix == max)
		{
			// done
			this.refreshHeaderBarCache();
			this.refreshTableCoords();
			SelectionManager.doSelectChecked();
			bShallowWalkDone = true;
			if (bDeepWalkAfter)
				this.startDeepWalker();
		}
		else
		{
			// walk the next piece of the grid
			setTimeout("GridControl.startShallowWalker("+cBugRows+","+nStep+","+bDeepWalkAfter+")", 5);
		}
	}
	
	this.startDeepWalker = function()
	{
		for (var ix = 0; ix < rgGridData.length; ix++)
		{
			this.walkTheRow(ix);
		}
		bDeepWalkDone = true;
	}
	
	this.isReady = function()
	{
		if (!bShallowWalkDone)
			return false; // Shallow walk isn't done, return false and let server-side handle request
		if (!bDeepWalkDone)
		{
			// Deep walk isn't done...if it can be done, return true, otherwise return false
			if (this.canDeepWalk())
				return true;
			else
				return false;
		}
		return true;
	}
	
	this.canDeepWalk = function()
	{
		var oTestRow = this.getRowNode(0);
		if (!oTestRow) return false;
		var vTestVal = XMLParser.getCustomTagValueFrom(oTestRow, "fb:cv"); // cell value
		return (null != vTestVal);
	}
	
	// walkTheRow
	//
	// Traverse over a single grid view row and collect
	// data from fb:cellvalue tags for JS array
	//
	this.walkTheRow = function(ixRow)
	{
		// Don't collect grid data if it's overflowing...we won't use JS to sort anyway
		if (!bSearchResult && bOverflow) return;
		
		if (!rgGridData) return;
		var oRow = GridControl.getRowNode(ixRow);
		if (!oRow) return;
		
		var rgTDs = oRow.cells;
		var ixCol = 0;
		var max = rgTDs.length;
		for (var ix = 0; ix < max; ix++)
		{
			var vVal = XMLParser.getCustomTagValueFrom(rgTDs[ix], "fb:cv"); // cell value
			if (vVal)
			{
				vVal = vVal.toLowerCase();
				rgGridData[ixRow][ixCol] = new Object();
				rgGridData[ixRow][ixCol].vVal = vVal;
				rgGridData[ixRow][ixCol].vSortBy = vVal;
				vVal = XMLParser.getCustomTagValueFrom(rgTDs[ix], "fb:csv"); // cell sort value
				if (vVal)	rgGridData[ixRow][ixCol].vSortBy = vVal.toLowerCase();
				
				ixCol++;
			}
		}
	}
	
	this.alertData = function()
	{
		// For debuggin'

		if (!rgGridData) return;
		var s = "";
		for (var r = 0; r < rgGridData.length; r++)
		{
			for (var c = 0; c < cCols; c++)
			{
				s += rgGridData[r][c].vVal + ", ";
			}
			s += "\n";
		}
		alert(s);
	}
	
	this.alertColumnInfo = function()
	{
		// For debuggin'

		if (!rgColInfo) return;
		var s = "";
		for (var ix = 0; ix < rgColInfo.length; ix++)
		{
			s += rgColInfo[ix].iType + "\n";
		}
		alert(s);
	}
	
	this.isSearchResult = function()
	{
		return bSearchResult;
	}

	this.isPrimaryReverseSort = function()
	{
		return bPrimaryReverseSort;
	}
	
	this.isOverflowing = function()
	{
		return bOverflow;
	}

	this.getTableRight = function()
	{
		if (!pxTableRight) return 0;
		return pxTableRight + GRID_PADDING;
	}
	
	// Associate ixBug to ixRow in mapBugToRow
	this.setBugToRow = function( ixBug, ixRow )
	{
		if (!mapBugToRow) mapBugToRow = new Array();
		mapBugToRow[ixBug] = ixRow;
	}
	
	// Lookup ixBug's associated ixRow
	this.rowFromBug = function(ixBug)
	{
		if (!mapBugToRow) return -1;
		return mapBugToRow[ixBug];
	}
	
	this.bugFromRow = function(ixRow)
	{
		if (!rgGridData) return -1;
		return rgGridData[ixRow].ixBug;
	}
	
	// sortBy
	//
	// 1) Sort grid data by ixColumn (stable sort)
	// 2) Update DOM appropriately
	//
	this.sortBy = function(sId)
	{	
		if (!bShallowWalkDone)
		{
			// Wait for grid walk to finish building internal model
			setTimeout("GridControl.sortBy('" + sId + "')", 500);
			return;
		}
		if (!bDeepWalkDone)
		{
			this.walkTheGrid(true);
		}
		setTimeout("GridControl.doSortBy('" + sId + "')", 1);
	}
	
	this.doSortBy = function(sId)
	{
		if (!oTable) return;
		var ixColumn = mapIdToCol[sId];
		bPrimaryReverseSort = (this.isPrimarySort(ixColumn) && !bPrimaryReverseSort);
		fDataTypeToSort = (rgColInfo[ixColumn].sType == "string") ? 1 
						  : (rgColInfo[ixColumn].sType == "date") ? 2 
						  : (rgColInfo[ixColumn].sType == "reversedate") ? 3
						  : (rgColInfo[ixColumn].sType == "date|string") ? 4
						  : 0;
		ixColToSort = ixColumn;
		rgSortType[0] = rgColInfo[ixColumn].iType;

		// Note: array.sort() is not a stable sort
		// in all browsers!  This is why we give each
		// object a reference to its own index in the array...
		// so we can use this data in sortHelper to keep things stable.
		rgGridData.sort(this.sortHelper);
		
		var i;
		var max = rgGridData.length;
		for (i = 0; i < max; i++)
		{
			// keep index self-references updated
			rgGridData[i].ixRow = i;
		}
		this.sortTheDOM();
		
		// Case 253369: blur() the sort link, b/c if focus remains
		// mousewheel/pgDown scrolling does not work in FF 1.0.7
		// due to parent div's overflow:hidden
		var oLink = elById(sId);
		if (oLink) oLink.blur();

		//Rebuild the grid keyboard browser
		setTimeout("KeyManager.setupGridBrowser()", 1);

		changeFilterName(Lang.getString("FB_FILTER"));
		Info.hide();
	}

	this.getColSignature = function()
	{
		this.getPixelWidthOrder();
		var s = "";
		if (!rgColInfo) return s;
		for (var ix = 0; ix < rgColInfo.length; ix++)
		{
			if (!rgColInfo[ix]) continue;
			if (s.length > 0) s += ",";
			s += rgColInfo[ix].iType + ":" + rgColInfo[ix].pxWidth;
		}
		return s;
	}

	this.getSortSignature = function()
	{
		if (!rgSortType || !rgSortType.length) return "";
		return rgSortType[0] + "," + rgSortType[1] + "," + rgSortType[2];
	}

	this.synchServerSort = function( oXML )
	{
		if (!oXML) return;
		for (var ix = 0; ix < 3; ix++)
		{
			var iType = XMLParser.getTextFrom(oXML, "sortType" + ix);
			if (iType) rgSortType[ix] = iType;
		}
		ListControl.updateSignature(false);
	}

	// isSortMatch
	//
	// Returns true if column types iTypeA and iTypeB 
	// have the same sort type.
	// Includes special handling for various Title col types (3, 23, 24)
	//
	this.isSortMatch = function(iTypeA, iTypeB)
	{
		return  (iTypeA == iTypeB) || 
				((iTypeA == 3 || iTypeA == 23 || iTypeA == 24) &&
				 (iTypeB == 3 || iTypeB == 23 || iTypeB == 24));
	}
	
	this.createSortArrow = function(ixCol, oParent)
	{
		if (!oSortArrowPrototype) oSortArrowPrototype = elById("changeSortArrowLink_prototype");
		if (ixCol == null || oParent == null || !oSortArrowPrototype) return null;
		
		var oArrowAndLink = oSortArrowPrototype.cloneNode(true);
		if (!oArrowAndLink) return null;
		var oImg = XMLParser.getNodeFrom(oArrowAndLink, "img");
		if (!oImg) return null;
		var oTextLink = XMLParser.getNodeUsingIdFrom(oParent, "a", "changeSortLink_"+ixCol);
		if (!oTextLink) return null;
		oArrowAndLink.href = oTextLink.href;
		oArrowAndLink.onclick = oTextLink.onclick;
		oImg.src = this.getSortArrowSrc();
		oArrowAndLink.id = "changeSortArrowLink_" + ixCol;
		return oArrowAndLink;
	}
	
	this.getSortArrowSrc = function()
	{
		var bArrowOrientation = bPrimaryReverseSort;
		// Sort arrow needs to be flipped for Last Updated column since it is reverse sorted by default on server
		if (rgSortType[0] == 15) bArrowOrientation = !bArrowOrientation; 
		return bArrowOrientation ? "images/sortdown-ico.gif" : "images/sortup-ico.gif";
	}
	
	this.sortHelper = function(a, b)
	{	
		var first = bPrimaryReverseSort ? b : a;
		var second = bPrimaryReverseSort ? a : b;
		
		if (fDataTypeToSort == 1)
		{
			// sorting strings
			if (first[ixColToSort].vSortBy < second[ixColToSort].vSortBy) return -1;
			if (first[ixColToSort].vSortBy > second[ixColToSort].vSortBy) return 1;
		}
		else if (fDataTypeToSort == 4)
		{
			// sorting by date as primary, string as secondary
			if (first[ixColToSort].vSortBy - second[ixColToSort].vSortBy < 0) return (fDataTypeToSort == 3) ? 1 : -1;
			if (first[ixColToSort].vSortBy - second[ixColToSort].vSortBy > 0) return (fDataTypeToSort == 3) ? -1 : 1;

			if (first[ixColToSort].vVal < second[ixColToSort].vVal) return -1;
			if (first[ixColToSort].vVal > second[ixColToSort].vVal) return 1;
		}
		else {
			// sorting ints or dates (remember to flip order if we're sorting reversedates)
			if (first[ixColToSort].vSortBy - second[ixColToSort].vSortBy < 0) return (fDataTypeToSort == 3) ? 1 : -1;
			if (first[ixColToSort].vSortBy - second[ixColToSort].vSortBy > 0) return (fDataTypeToSort == 3) ? -1 : 1;
		}
		return a.ixRow - b.ixRow; // make sure sort is stable
	}
	
	// sortTheDOM
	//
	// Display sort results by swapping DOM table rows and inserting groups accordingly
	//
	this.sortTheDOM = function()
	{	
		var vLast = null;
		var vCurrent = null;
		var cGroups = 0;
		var oGroupCurrent, oGroupHeaderNew;
		
		this.moveTableToHold();
		var oTableNew = oTable.cloneNode(false);
		
		var bGroupsEnabled = (rgColInfo[ixColToSort].sHeader && 
							  rgColInfo[ixColToSort].sHeader != " ");
		
		if (!bGroupsEnabled)
		{
			oGroupCurrent = document.createElement("tbody");
			oGroupCurrent.id = "group_1";
			oGroupHeaderNew = this.createGroupHeader(1, null);
			oTableNew.insertBefore(oGroupHeaderNew, null);
			oTableNew.insertBefore(oGroupCurrent, null);
			cGroups = 1; // the whole grid is a group
		}
		
		var max = rgGridData.length;
		var ixBug, oNewHeader, oNewNode;
		for (var ixRowNew = 0; ixRowNew < max; ixRowNew++)
		{
			oNewHeader = null;
			vCurrent = rgGridData[ixRowNew][ixColToSort].vVal;
			if (bGroupsEnabled && vCurrent != vLast)
			{
				// Form new group
				cGroups++;
				
				oGroupCurrent = document.createElement("tbody")
				oGroupCurrent.id = "group_"+cGroups;
				oGroupHeaderNew = this.createGroupHeader(cGroups, vCurrent);
				oTableNew.insertBefore(oGroupHeaderNew, null);
				oTableNew.insertBefore(oGroupCurrent, null);
			}
			
			ixBug = rgGridData[ixRowNew].ixBug;
			
			// 1) Get next row for table
			oNewNode = this.getSwapNode(ixBug, ixRowNew, cGroups);
			// 2) Insert next row in table
			oGroupCurrent.insertBefore(oNewNode, null);
			// 3) Update ixBug-ixRow map
			this.setBugToRow(ixBug, ixRowNew);
			
			vLast = vCurrent;
		}
		
		// If truncated Spam count was displayed before the sort,
		// keep this part of the table around
		var oRowSpamCount = elById("row_spam");
		if (oRowSpamCount) 
		{
			oGroupCurrent = document.createElement("tbody");
			oGroupCurrent.insertBefore(oRowSpamCount, null);
			oTableNew.insertBefore(oGroupCurrent, null);
		}
		
		// Make the big switcheroo
		oTable = oTableNew;
		this.restoreTableFromHold();
		
		this.refreshRowNodeCache();
		this.refreshHeaderBarCache();
		this.refreshTableCoords();
		
		// Highlight the whole grid to fix the post-sort color scheme
		SelectionManager.highlightAll(0, rgGridData.length);
	}
	
	// refreshRowNodeCache
	//
	// Walk through the grid and refresh the cache of pointers
	// to row nodes
	//
	this.refreshRowNodeCache = function()
	{
		var oRow;
		var cBugRows = 0;
		var cGroups = 0;
		var rgTRs = oTable.getElementsByTagName("tr");
		rgRowNodeCache = new Array();
		for (var ix = 0; ix < rgTRs.length; ix++)
		{
			oRow = rgTRs[ix];
			if (oRow.id == ("row_"+cBugRows))
			{
				rgRowNodeCache[cBugRows] = oRow;
				cBugRows++;
			}
		}
	}
	
	// refreshHeaderBarCache
	//
	// Refresh node references to all the group header bars
	//	
	this.refreshHeaderBarCache = function()
	{
		rgHeaderBarCache = new Array();
		mapIdToCol = new Array(); // refresh header bar links
		var oGroupHeaderBar = XMLParser.getNodeUsingIdFrom(oTable, "tr", "groupHeaderBar_All");
		if (oGroupHeaderBar)
		{
			rgHeaderBarCache[0] = oGroupHeaderBar;
			this.mapColumnIdsToCols(oGroupHeaderBar);
		}
		else
		{
			for (var ix = 0; ix == 0 || oGroupHeaderBar; ix++)
			{
				oGroupHeaderBar = XMLParser.getNodeUsingIdFrom(oTable, "tr", "groupHeaderBar_"+(ix+1));
				if (oGroupHeaderBar)
				{
					rgHeaderBarCache[ix] = oGroupHeaderBar;
					if (ix == 0)
						this.mapColumnIdsToCols(oGroupHeaderBar);
				}
			}
		}
		var oHeaderTest = elById("groupHeader_prototype");
		if (oHeaderTest) oHeaderPrototype = oHeaderTest;
		if (oHeaderPrototype) oGroupHeaderBar = XMLParser.getNodeUsingIdFrom(oHeaderPrototype, "tr", "groupHeaderBar_prototype");
		if (oGroupHeaderBar)
			if (oGroupHeaderBar) rgHeaderBarCache[rgHeaderBarCache.length] = oGroupHeaderBar;
			
		this.refreshColumnHotSpots();
	}
	
	this.refreshTableCoords = function()
	{	
		if (!oTable) return;
		var oNodeFirst = rgHeaderBarCache[0];
		if (oNodeFirst && oNodeFirst.offsetHeight)
		{
			pxTableTop = calculateOffset(oNodeFirst, "offsetTop");
		}
		else if (oNodeFirst)
		{
			// Safari doesn't like giving offsetHeight/Width of tr/td elements
			oNodeFirst = XMLParser.getNodeFrom(oNodeFirst, "div");
			pxTableTop = calculateOffset(oNodeFirst, "offsetTop") - GRID_PADDING;
		}
		var oNodeLast = this.getRowNode(rgGridData.length-1);
		if (oNodeLast && oNodeLast.offsetHeight)
		{
			pxTableBottom = calculateOffset(oNodeLast, "offsetTop") + oNodeLast.offsetHeight;
		}
		else if (oNodeLast)
		{
			// Safari doesn't like giving offsetHeight/Width of tr/td elements
			oNodeLast = XMLParser.getNodeFrom(oNodeLast, "div");
			pxTableBottom = calculateOffset(oNodeLast, "offsetTop") + oNodeLast.offsetHeight + GRID_PADDING;
		}
		
		pxTableWidth = oTable.offsetWidth;
		pxTableLeft = calculateOffset(oTable, "offsetLeft");
		pxTableRight = pxTableLeft + pxTableWidth;
	}
	
	// mapColumnIdsToCols
	//
	// Refresh map of column header id's to col indexes
	//
	this.mapColumnIdsToCols = function(oRow)
	{
		var rgTHs = XMLParser.getNodeArrayFrom(oRow, "th");
		var oObj, oCol;
		var cColumnsFound = 0;
		for (var ix = 0; ix < rgTHs.length; ix++)
		{
			oCol = rgTHs[ix];
			if (oCol.id.indexOf("col") == 0)
			{
				mapIdToCol[oCol.id] = cColumnsFound;
				oObj = XMLParser.getNodeUsingIdFrom(oCol, "a", "changeSortLink_"+cColumnsFound);
				if (oObj) mapIdToCol[oObj.id] = cColumnsFound;
				oObj = XMLParser.getNodeUsingIdFrom(oCol, "a", "changeSortArrowLink_"+cColumnsFound);
				if (oObj) mapIdToCol[oObj.id] = cColumnsFound;
				oObj = XMLParser.getNodeUsingIdFrom(oCol, "th", "gripResize_"+cColumnsFound);
				if (oObj) mapIdToCol[oObj.id] = cColumnsFound;
				cColumnsFound++;
			}
		}
	}
	
	this.refreshColumnHotSpots = function()
	{
		var oRow = rgHeaderBarCache[0];
		rgHotSpots = new Array();
		var rgTHs = XMLParser.getNodeArrayFrom(oRow, "th");
		var oCol;
		var cColumnsFound = 0;
		var max = rgTHs.length;
		for (var ix = 0; ix < max; ix++)
		{
			oCol = rgTHs[ix];
			if (oCol.id.indexOf("col") == 0)
			{	
				rgHotSpots[cColumnsFound] = new Object();
				rgHotSpots[cColumnsFound].left = calculateOffset(oCol, "offsetLeft");
				rgHotSpots[cColumnsFound].right = rgHotSpots[cColumnsFound].left + oCol.offsetWidth;
				rgHotSpots[cColumnsFound].middle = (rgHotSpots[cColumnsFound].left + rgHotSpots[cColumnsFound].right) / 2;
				cColumnsFound++;
			}
		}
	}
	
	// getSwapNode
	//
	// Get a copy of the ixBug row node that belongs in ixRowTarget
	// after sorting, and update its properties to reflect the swap
	//
	this.getSwapNode = function(ixBug, ixRowTarget, ixGroup)
	{
		var ixRowOld = this.rowFromBug(ixBug);
		var oNodeOld = this.getRowNode(ixRowOld);
		var oNodeDue = oNodeOld.cloneNode(true);
		var oBoxOld = XMLParser.getNodeFrom(oNodeOld, "input");
		var oBoxDue = XMLParser.getNodeFrom(oNodeDue, "input");
		oBoxDue.defaultChecked = oBoxOld.checked;
		oNodeDue.id = "row_"+ixRowTarget;
		oNodeDue.fAltClass = (ixRowTarget % 2);
		return oNodeDue;
	}
	
	// createGroupHeader
	//
	// Create new group header for grid view by formatting empty prototype
	//
	this.createGroupHeader = function(ixGroup, vVal)
	{
		var oHeaderTest = elById("groupHeader_prototype");
		if (oHeaderTest) oHeaderPrototype = oHeaderTest;
		if (!oHeaderPrototype) return null;
		
		var oNewHeader = oHeaderPrototype.cloneNode(true);
		var oNewBox = XMLParser.getNodeFrom(oNewHeader, "input");
		if (!oNewBox.id == "groupBox_prototype") return null;
		var oNewAnchor = XMLParser.getNodeFrom(oNewHeader, "a");
		if (!oNewAnchor.id == "groupAnchor_prototype") return null;
		var oNewHeaderBar = XMLParser.getNodeUsingIdFrom(oNewHeader, "tr", "groupHeaderBar_prototype");
		if (!oNewHeaderBar) return null;
		
		oNewHeader.id = "groupHeader_"+ixGroup;
		oNewBox.id = "groupBox_"+ixGroup;
		oNewAnchor.id = "groupAnchor_"+ixGroup;
		oNewHeaderBar.id = "groupHeaderBar_"+ixGroup;
		
		this.setSortArrows(oNewHeaderBar);
		
		// Set group title
		var sHeaderTemplate = rgColInfo[ixColToSort].sHeader;
		var sHeader;
		if (sHeaderTemplate && sHeaderTemplate != " ")
			sHeader = swap1(rgColInfo[ixColToSort].sHeader, vVal);
		else
			sHeader = Lang.getString("FB_BUGZ");
			
		XMLParser.setCustomTagValue(oNewHeader, "fb:groupvalue", sHeader);
		oNewHeader.style.display = "";
		return oNewHeader;
	}
	
	this.setSortArrows = function(oHeaderBar)
	{
		var rgCols = XMLParser.getNodeArrayFrom(oHeaderBar, "th");
		var ixCol = 0;
		var oCol, oNoBr, oImg;
		for (var ix = 0; ix < rgCols.length; ix++)
		{
			oCol = rgCols[ix];
			if (oCol.id && oCol.id.indexOf("col") == 0)
			{
				oNoBr = XMLParser.getNodeFrom(oCol, "nobr");
				if (!oNoBr) continue;
				oImg = XMLParser.getNodeUsingIdFrom(oNoBr, "img", "sortArrow");
				
				if (oImg)
				{
					// sort arrow exists
					if (this.isPrimarySort(ixCol))
					{
						oImg.src = this.getSortArrowSrc();
					}
					else
					{
						oImg.parentNode.removeChild(oImg);
					}
				}
				else if (this.isPrimarySort(ixCol))
				{
					oNoBr.insertBefore(this.createSortArrow(ixCol, oNoBr), null);
				}
				ixCol++;
			}
		}
	}
	
	// getRowNode - return n'th bug row node (use rgRowNodeCache)
	this.getRowNode = function(n)
	{	
		if (!rgRowNodeCache || !rgRowNodeCache[n]) return null;
		return rgRowNodeCache[n];
	}
	
	// removeColumn
	//
	// Remove column from grid and 
	// tell FogBugz to save the layout
	//
	this.removeColumn = function(ixColumn)
	{	
		if (!bShallowWalkDone)
		{
			// Wait for grid walk to finish building internal model
			setTimeout("GridControl.removeColumn(" + ixColumn + ")", 500);
			return;
		}
		this.sendRemoveColumnRequest(ixColumn);
		
		var iType = rgColInfo[ixColumn].iType;
		this.styleColumnPopupLinkByType(iType, false);
		this.removeColumnFromDOM(ixColumn);
		this.removeColumnInfo(ixColumn);
		Info.hide();
		ListControl.updateSignature(true);
	}
	
	this.sendRemoveColumnRequest = function(ixColumn)
	{
		if (!oAjaxGridRemove) oAjaxGridRemove = new AjaxRequest();
		
		if (oAjaxGridRemove.isBusy())
		{
			// request is already busy, wait half a sec and try again
			setTimeout('GridControl.sendRemoveColumnRequest('+ixColumn+')', 500);
		}
		else
		{
			// fire & forget
			oAjaxGridRemove.initialize();
			oAjaxGridRemove.bDefaultFailureBehavior = false;
			oAjaxGridRemove.open("GET", sDefaultURI + "?fAlaCarte=1&pre=preRemoveColumn&ixColumn="+ixColumn, true);
			oAjaxGridRemove.send(null);
		}
	}
	
	// removeColumnFromDOM
	//
	// Remove entire column from grid via DOM
	//
	this.removeColumnFromDOM = function(ixColumn)
	{
		this.moveTableToHold();
		
		var oRow, ixBug, ix;
		for (ix = 0; ix < rgGridData.length; ix++)
		{
			this.removeColumnFromRow(ix, ixColumn, false);
		}
		for (ix = 0; ix < rgHeaderBarCache.length; ix++)
		{
			this.removeColumnFromRow(ix, ixColumn, true);
		}
		
		this.restoreTableFromHold();
		
		this.refreshColumnHotSpots();
		this.refreshTableCoords();
		ListControl.extendNavBarSpacer(this.getTableRight());
	}
	
	// removeColumnFromRow
	//
	// Remove column from specific row or headerbar
	//
	this.removeColumnFromRow = function(ixRow, ixColumn, bHeaderBar)
	{
		var oCol, oBox;
		var oRow = bHeaderBar ? rgHeaderBarCache[ixRow] : this.getRowNode(ixRow);
		var sId = bHeaderBar ? "colHead_"+ixColumn : "col_"+ixColumn;
		
		var rgTDs = oRow.getElementsByTagName(bHeaderBar ? "th" : "td");
		var cColumnsAfter = 0;
		var cColumnsFound = 0;
		var bRemoved = false;
		
		// Remove the column from row and update the properties of
		// remaining columns
		for (var ix = 0; ix < rgTDs.length; ix++)
		{
			oCol = rgTDs[ix];
			if (oCol.id.indexOf("col") == 0)
			{
				// Correct the id's of columns to the right of deleted column
				if (cColumnsFound > ixColumn)
				{
					var ixNew = (ixColumn+cColumnsAfter);
					oCol.id = bHeaderBar ? "colHead_"+ixNew 
										 : "col_"+ixNew;
					cColumnsAfter++;
					
					if (bHeaderBar)
					{
						oBox = XMLParser.getNodeUsingIdFrom(oCol, "a", "changeSortLink_"+cColumnsFound);
						if (oBox)	oBox.id = "changeSortLink_"+ixNew;
					}
				}
				cColumnsFound++;
			}
			if (oCol.id == sId && !bRemoved)
			{
				oRow.removeChild(oCol);
				ix--;
				bRemoved = true;
			}
		}
	}
	
	// toggleColumn
	//
	// Toggle visibility of column type,
	// if iType is not already on grid, add it
	// else remove it
	//
	this.toggleColumn = function(iType)
	{
		var oBox = elById("columnPopupBox_"+iType);
		if (!oBox) return;
		oBox.checked = !oBox.checked;
		var oSelf = this;
		if (oBox.checked)
		{
			Process.start(this, "resultAddColumn", function() {oSelf.failureAddColumn(iType)}, Lang.getString("FB_ADDING_COLUMN"));
			setTimeout("GridControl.addColumn(" + iType + ")", 1);
		}
		else
		{
			var ix;
			for (ix = 0; ix < rgColInfo.length; ix++)
			{
				if (rgColInfo[ix] && rgColInfo[ix].iType == iType)
				{
					Info.show(Lang.getString("FB_REMOVING_COLUMN"));
					setTimeout("GridControl.removeColumn(" + ix + ")", 1);
					break;
				}
			}
		}
	}
	
	// addColumn
	//
	// Add a new column to the existing grid
	//
	this.addColumn = function(iType)
	{
		ListControl.showLoadingDiv();
				
		if (!bShallowWalkDone)
		{
			// Wait for grid walk to finish building internal model
			setTimeout("GridControl.addColumn(" + iType + ")", 500);
			return;
		}
		
		if (!oAjaxGridAdd) oAjaxGridAdd = new AjaxRequest();
		if (oAjaxGridAdd.isBusy())
		{
			// request is already busy, wait half a sec and try again
			setTimeout('GridControl.addColumn('+iType+')', 500);
		}
		else
		{
			setBugValues(true);
			oAjaxGridAdd.initialize();
			oAjaxGridAdd.onreadystatechange(this.resultAddColumn);
			var oSelf = this;
			oAjaxGridAdd.onfailure(function(){oSelf.failureAddColumn(iType)});
			oAjaxGridAdd.open("POST", sDefaultURI, true);
			oAjaxGridAdd.send("fAlaCarte=1&pre=preAddColumn&pg=pgShowAlaCarteColumn&iType=" + iType + "&ixBug=" + document.bulkForm.ixBug.value);
			this.styleColumnPopupLinkByType(iType, true);
		}
	}
	
	// onreadystatechange handler for GridControl.addColumn
	this.resultAddColumn = function()
	{
		// Have to call GridControl.function(), not this.function()
		// b/c onreadystatechange handlers don't get object context
		GridControl.addColumnToDOM(oAjaxGridAdd.responseXML());
		ListControl.hideLoadingDiv();
		ListControl.updateSignature(true);
	}

	this.failureAddColumn = function( iType )
	{
		GridControl.styleColumnPopupLinkByType(iType, false);
		ListControl.hideLoadingDiv();
	}
	
	// moveTableToHold
	//
	// Before performing lots of operations on the table subtree,
	// use this fxn to remove it from the DOM 
	//
	this.moveTableToHold = function()
	{
		oParentHolder = oTable.parentNode;
		oSiblingHolder = oTable.nextSibling;
		oParentHolder.removeChild(oTable);
	}
	
	// restoreTableFromHold
	//
	// Restore table to DOM from temporary holding
	//
	this.restoreTableFromHold = function()
	{
		oParentHolder.insertBefore(oTable, oSiblingHolder);
	}
	
	// addColumnToDOM
	//
	// Use XML payload to add new column
	// to grid via DOM
	//
	this.addColumnToDOM = function(oXML)
	{
		this.moveTableToHold();
	
		var rgBug = XMLParser.getNodeArrayFrom(oXML, "Bug");
		var oBug, ixBug, ixRow, sHTML, sHTMLColHeader, pxWidth, oRow, sType, iType, sGroupHeading, oDiv;
		var ixColumnNew = cCols;
		
		sHTMLColHeader =	XMLParser.getCDataFrom(oXML, "sHTMLColHeader");
		pxWidth =		XMLParser.getTextFrom(oXML, "pxWidth");
		sGroupHeading =		XMLParser.getTextFrom(oXML, "sGroupHeading");
		sType =			XMLParser.getTextFrom(oXML, "sType");
		iType =			XMLParser.getTextFrom(oXML, "iType");	

		this.addColumnInfo(sGroupHeading, sType, iType);
		
		// Make a new column header
		var oColHeaderOld = XMLParser.getNodeUsingIdFrom(oHeaderPrototype, "th", "header_prototype");
		var oColHeaderNew = oColHeaderOld.cloneNode(true);
		
		oColHeaderNew.style.display = "";
		oColHeaderNew.style.width = pxWidth + "px";
		oColHeaderNew.id = "colHead_"+ixColumnNew;
		oColHeaderNew.innerHTML = sHTMLColHeader;
		oDiv = XMLParser.getNodeFrom(oColHeaderNew, "div");
		if (oDiv) oDiv.style.width = pxWidth + "px";

		if (this.isPrimarySort(cCols-1))
		{
			var oNoBr = XMLParser.getNodeFrom(oColHeaderNew, "nobr");
			var oArrow = this.createSortArrow(cCols-1, oNoBr);
			if (oNoBr && oArrow) oNoBr.insertBefore(oArrow, null);
		}
		
		// Insert new column header in each group's column header row
		if (!rgHeaderBarCache) this.refreshHeaderBarCache();
		var ix, oSpacer;
		for (ix = 0; ix < rgHeaderBarCache.length; ix++)
		{
			oRow = rgHeaderBarCache[ix];
			// Use XMLParser instead of oRow.cells b/c Safari 1.2 won't include <th> elems
			oSpacer = XMLParser.getNodeUsingIdFrom(oRow, "th", "r-s");
			oRow.insertBefore(oColHeaderNew.cloneNode(true), oSpacer);
		}
		
		// Insert new column in each row
		var oColNew = document.createElement("td");
		for (ix = 0; rgBug && ix < rgBug.length; ix++)
		{
			if (ix > 0) oColNew = oColNew.cloneNode(false);
			oBug = rgBug[ix];
			ixBug = XMLParser.getTextFrom(oBug, "ixBug");
			sHTML = XMLParser.getCDataFrom(oBug, "sHTML");
			oColNew.id = "col_"+ixColumnNew;
			oColNew.style.width = pxWidth + "px";
			oColNew.innerHTML = sHTML;
			oDiv = XMLParser.getNodeFrom(oColNew, "div");
			if (oDiv) oDiv.style.width = pxWidth + "px";
			ixRow = this.rowFromBug(ixBug);
			oRow = this.getRowNode(ixRow);
			if (oRow.bgCurrent)
				oColNew.style.backgroundColor = oRow.bgCurrent;
			else
				oColNew.style.backgroundColor = "";
			oRow.insertBefore(oColNew, oRow.cells[oRow.cells.length-1]);
		}
		
		// Add any hoverspans needed for this column that we may be missing
		ListControl.addHoverSpansFromXML(oXML);
		// Put the table back
		this.restoreTableFromHold();
		// Update coordinates
		this.refreshTableCoords();
		ListControl.extendNavBarSpacer(this.getTableRight());
		// Update internal JS model of grid
		this.refreshHeaderBarCache();
		this.walkTheGrid(true);
	}
	
	// startColumnDrag
	//
	// Begin dragging a column header for
	// possible reorganization
	//
	this.startColumnDrag = function(oHeader)
	{
		if (!oHeader || !mapIdToCol) return;
		ixColToDrag = mapIdToCol[oHeader.id];
		if (ixColToDrag == null) return;
		bCancelEvents = true;
		oGhostDiv.style.height = (oHeader.offsetHeight - ((window.safari ? -1 : 1) * (GRID_PADDING/2))) + "px";
		oGhostDiv.style.width = (oHeader.offsetWidth - ((window.safari ? -1 : 1) * (GRID_PADDING/2))) + "px";
		oGhostDiv.innerHTML = oHeader.innerHTML;
		var o = XMLParser.getNodeFrom(oGhostDiv, "a");
		if (o)	o.style.color = "#fff";
		pxDragOffsetX = xMouse - calculateOffset(oHeader, "offsetLeft");
		pxDragOffsetY = yMouse - calculateOffset(oHeader, "offsetTop");
		pxDragStartX = xMouse;
		pxDragStartY = yMouse;
		bColDragging = true;
		bColDragMoved = false;
	}
	
	// stopColumnDrag
	//
	// Stop dragging column header, hide all ghost divs
	//
	this.stopColumnDrag = function()
	{
		bCancelEvents = false;
		if (!bShallowWalkDone)
		{
			// Wait for grid walk to finish building internal model
			setTimeout("GridControl.stopColumnDrag()", 500);
			return;
		}
		
		if (bColDragMoved)
		{
			var ix = this.targetOfHotSpot();
			if (ix != null && 
				ixColToDrag != ix &&
				ixColToDrag != (ix-1))
			{
				Info.show(Lang.getString("FB_MOVING"), null, null);
				setTimeout("GridControl.doColumnMove(" + ixColToDrag + "," + ix + ")", 1);
			}
			this.hideGhostDivs();
			document.body.style.cursor = "auto";
		}
		bColDragging = false;
		bColDragMoved = false;
	}
	
	this.doColumnMove = function(ixColToDrag, ix)
	{
		this.moveColumnInfo(ixColToDrag, ix);
		this.sendMoveColumnRequest(ixColToDrag, ix);
		this.moveColumnBefore(ixColToDrag, ix);
		this.mapColumnIdsToCols(rgHeaderBarCache[0]);
		this.moveGridData(ixColToDrag, ix);
		Info.hide();
		ListControl.updateSignature(true);
	}
	
	// cancelActions
	//
	// Cancel column dragging or resizing b/c esc key was hit
	//
	this.cancelActions = function()
	{
		bColDragging = false;
		bColDragMoved = false;
		bResizing = false;
		bResizeMoved = false;
		bCancelEvents = false;
		this.hideGhostDivs();
		document.body.style.cursor = "auto";
	}
	
	this.isDragOverThreshold = function()
	{
		return	((Math.abs(xMouse - pxDragStartX)) > pxDragThreshold) ||
				((Math.abs(yMouse - pxDragStartY)) > pxDragThreshold);
	}
	
	// notifyMovement
	//
	// Notify grid control of mousemove so that
	// it can position the column header ghost divs
	// appropriately, if currently dragging
	//
	this.notifyMovement = function()
	{
		if (bColDragging)
		{
			removeTextSelections();
			
			document.body.style.cursor = "move";
			if (oGhostDiv.style.display == "" || this.isDragOverThreshold())
			{
				bColDragMoved = true;
				oGhostDiv.style.display = "";
				oGhostDiv.style.top = (yMouse-pxDragOffsetY) + "px";
				oGhostDiv.style.left = Math.min((xMouse-pxDragOffsetX), pxTableRight) + "px";
				
				var ix = this.targetOfHotSpot();
				if (ix != null)
				{
					if (ix >= rgHotSpots.length)
					{
						oGhostLine.style.left = rgHotSpots[rgHotSpots.length-1].right-1 + "px";
						oGhostDownArrow.style.left = rgHotSpots[rgHotSpots.length-1].right-1 - (oGhostDownArrow.offsetWidth/2) + "px";
					}
					else if (ix == 0)
					{
						oGhostLine.style.left = rgHotSpots[0].left-1 + "px";
						oGhostDownArrow.style.left = rgHotSpots[0].left-1- (oGhostDownArrow.offsetWidth/2) + "px";
					}
					else
					{
						oGhostLine.style.left = rgHotSpots[ix].left-1 + "px";
						oGhostDownArrow.style.left = rgHotSpots[ix].left-1 - (oGhostDownArrow.offsetWidth/2) + "px";
					}
					
					oGhostLine.style.height = (pxTableBottom - pxTableTop) + "px";
					oGhostLine.style.top = pxTableTop + "px";
					oGhostLine.style.display = "";
					oGhostDownArrow.style.display = "";
					oGhostDownArrow.style.top = pxTableTop - oGhostDownArrow.offsetHeight + "px";
					oGhostDownArrow.style.visibility = "visible";
				}
			}
			else
			{
				oGhostLine.style.display = "none";
				oGhostDownArrow.style.display = "none";
				oGhostDownArrow.style.visibility = "hidden";
			}
		}
		
		if (bResizing)
		{
			removeTextSelections();
		
			if (!bResizeMoved)
			{
				var oHeaderTemp = rgHeaderBarCache[0];
				oHeaderToResize = XMLParser.getNodeUsingIdFrom(oHeaderTemp, "th", "colHead_"+ixColToResize);
				if (!oHeaderToResize) return;
				pxResizeOffset = calculateOffset(oHeaderToResize, "offsetLeft");
		
				oGhostArea.style.left = pxResizeOffset + "px";
				oGhostArea.style.top = pxTableTop + "px";
				oGhostArea.style.height = (pxTableBottom - pxTableTop) + "px";
				oGhostArea.style.width = oHeaderToResize.offsetWidth + "px";
				oGhostArea.style.display = "";
		
				this.showLeftRightGhostArrows();
				bResizeMoved = true;
			}
			
			oGhostArea.style.display = "";
			oGhostArea.style.width = Math.max((xMouse-pxResizeOffset),MIN_COL_WIDTH) + "px";
			this.showLeftRightGhostArrows();
		}
	}
	
	this.showLeftRightGhostArrows = function()
	{
		oGhostRightArrow.style.display = "";
		oGhostLeftArrow.style.display = "";
		oGhostRightArrow.style.visibility = "visible";
		oGhostLeftArrow.style.visibility = "visible";
		oGhostLeftArrow.style.top = pxTableTop - oGhostLeftArrow.offsetHeight + "px";
		oGhostRightArrow.style.top = pxTableTop - oGhostRightArrow.offsetHeight + "px";
		oGhostLeftArrow.style.left = (Math.max(xMouse,(pxResizeOffset+15))-oGhostLeftArrow.offsetWidth) + "px";
		oGhostRightArrow.style.left = Math.max(xMouse,(pxResizeOffset+15)) + "px";
	}
	
	// targetOfHotSpot
	//
	// Return the index of the current hotspot target
	// pointed to by the mouse when column dragging
	//
	this.targetOfHotSpot = function()
	{
		var o;
		for (var ix = 0; ix < rgHotSpots.length; ix++)
		{
			o = rgHotSpots[ix];
			if (xMouse >= o.left && xMouse < o.right)
			{
				return (xMouse > o.middle) ? (ix+1) : ix;
			}
			if (ix == 0 && xMouse < o.middle)	return 0;
			if (ix == (rgHotSpots.length-1) && xMouse > o.middle) return (ix+1);
		}
		return null;
	}
	
	// sendMoveColumnRequest
	//
	// Notify FogBugz that user's columns have been reordered
	//
	this.sendMoveColumnRequest = function(ixColToMove, ixColTarget)
	{
		if (ixColToMove == ixColTarget || ixColToMove == (ixColTarget-1)) return;
		
		if (!oAjaxGridReorder) oAjaxGridReorder = new AjaxRequest();
		if (oAjaxGridReorder.isBusy())
		{
			// request is already busy, wait half a sec and try again
			setTimeout('GridControl.sendMoveColumnRequest('+ixColToMove+','+ixColTarget+')', 500);
		}
		else
		{
			var s = "?fAlaCarte=1&pre=preReorderColumns&iTypeOrder=" + this.getColTypeOrder();
			oAjaxGridReorder.initialize();
			oAjaxGridReorder.bDefaultFailureBehavior = false;
			// no onreadystatechange handler...this is fire & forget
			oAjaxGridReorder.open("GET", sDefaultURI + s, true);
			oAjaxGridReorder.send(null);
		}
	}
	
	// getColTypeOrder
	//
	// Return comma-delimited list of column types
	// in order of the representation on the grid
	//
	this.getColTypeOrder = function()
	{
		var s = "";
		for (var ix = 0; ix < rgColInfo.length; ix++)
		{
			if (rgColInfo[ix] != null)
			{
				s += rgColInfo[ix].iType + ","
			}
		}
		if ( s.length > 0 )
			s = s.slice(0, -1);
		return s;
	}
	
	// moveColumnBefore
	//
	// Move entire column ixColToMove into the spot before ixColTarget
	//
	this.moveColumnBefore = function(ixColToMove, ixColTarget)
	{
		if (ixColToMove == ixColTarget || ixColToMove == (ixColTarget-1)) return;
		
		this.moveTableToHold();
		
		var ix;
		var max = rgGridData.length;
		for (ix = 0; ix < max; ix++)
		{
			this.moveColumnInRow(ix, ixColToMove, ixColTarget, false);
		}
		max = rgHeaderBarCache.length;
		for (ix = 0; ix < max; ix++)
		{
			this.moveColumnInRow(ix, ixColToMove, ixColTarget, true);
		}
		
		this.restoreTableFromHold();
		this.refreshColumnHotSpots();
	}
	
	// moveColumnInRow
	//
	// Move column in specific row from ixColToMove to spot before ixColTarget
	//
	this.moveColumnInRow = function(ixRow, ixColToMove, ixColTarget, bHeaderBar)
	{
		var oCol, oColTarget, oLink, rgLinks;
		var oRow = bHeaderBar ? rgHeaderBarCache[ixRow] : this.getRowNode(ixRow);
		var sId = bHeaderBar ? "colHead_"+ixColToMove : "col_"+ixColToMove;
		var sIdTarget = bHeaderBar ? "colHead_"+ixColTarget : "col_"+ixColTarget;
		
		var rgTDs = oRow.getElementsByTagName(bHeaderBar ? "th" : "td");
		var cColumnsAfter = 0;
		var cColumnsFound = 0;
		
		oCol = XMLParser.getNodeUsingIdFrom(oRow, (bHeaderBar ? "th" : "td"), sId);
		oColTarget = XMLParser.getNodeUsingIdFrom(oRow, (bHeaderBar ? "th" : "td"), sIdTarget);
		if (oColTarget == null) oColTarget = XMLParser.getNodeUsingIdFrom(oRow, (bHeaderBar ? "th" : "td"), "r-s");
		oRow.insertBefore(oCol, oColTarget);
		
		var ixRepairFrom = Math.min(ixColTarget, ixColToMove);
		
		// Update the properties of right-hand columns
		for (var ix = 0; ix < rgTDs.length; ix++)
		{
			oCol = rgTDs[ix];
			if (oCol.id.indexOf("col") == 0)
			{
				if (cColumnsFound >= ixRepairFrom)
				{
					// Correct the id's of columns in need of repair
					var ixNew = (ixRepairFrom+cColumnsAfter);
					oCol.id = bHeaderBar ? "colHead_"+ixNew 
					 					 : "col_"+ixNew;
					if (bHeaderBar)
					{
						rgLinks = XMLParser.getNodeArrayFrom(oCol, "a");
						for (var ixLink = 0; ixLink < rgLinks.length; ixLink++)
						{
							oLink = rgLinks[ixLink];
							if (oLink && oLink.id)
							{
								oLink.id = oLink.id.substring(0,oLink.id.indexOf("_")+1) + ixNew;
							}
						}
					}
					cColumnsAfter++;
				}
				cColumnsFound++;
			}
		}
	}
	
	this.startColumnResize = function(sId)
	{
		ixColToResize = mapIdToCol[sId];
		if (ixColToResize == null) return;
		
		bResizeMoved = false;
		bCancelEvents = true;
		bResizing = true;
	}
	
	this.stopColumnResize = function()
	{
		bCancelEvents = false;
		if (!bResizing) return;
		
		if (!bShallowWalkDone)
		{
			// Wait for grid walk to finish building internal model
			setTimeout("GridControl.stopColumnResize()", 500);
			return;
		}
		
		var w = oGhostArea.offsetWidth;
		
		if (bResizeMoved)
		{
			Info.show(Lang.getString("FB_RESIZING"), null, null);
			setTimeout("GridControl.doColumnResize(" + w + ")", 1);
		}
		
		bResizing = false;
		bResizeMoved = false;
		this.hideGhostDivs();
	}
	
	this.doColumnResize = function( w )
	{
		this.resizeColumn(w - GRID_PADDING);
		this.refreshColumnHotSpots();
		this.sendResizeColumnRequest();
		Info.hide();
		ListControl.updateSignature(false);
	}
	
	// sendResizeColumnRequest
	//
	// Notify FogBugz that user's columns have been resized
	//
	this.sendResizeColumnRequest = function()
	{	
		if (!oAjaxGridResize) oAjaxGridResize = new AjaxRequest();
		if (oAjaxGridResize.isBusy())
		{
			// request is already busy, wait half a sec and try again
			setTimeout('GridControl.sendResizeColumnRequest()', 500);
		}
		else
		{
			var s = "?fAlaCarte=1&pre=preResizeColumns&pxWidthOrder=" + this.getPixelWidthOrder();
			oAjaxGridResize.initialize();
			oAjaxGridResize.bDefaultFailureBehavior = false;
			// no onreadystatechange handler...this is fire & forget
			oAjaxGridResize.open("GET", sDefaultURI + s, true);
			oAjaxGridResize.send(null);
		}
	}
	
	// getPixelWidthOrder
	//
	// Return comma-delimited list of column widths
	// in order of the representation on the grid
	//
	this.getPixelWidthOrder = function()
	{
		var oRow = this.getRowNode(0);
		if (!oRow) return "";
		
		var s = "";
		var rgCols = XMLParser.getNodeArrayFrom(oRow, "td");
		var oCol, oDiv;
		var nColumnsFound = 0;
		
		for (var ix = 0; ix < rgCols.length; ix++)
		{
			oCol = rgCols[ix];
			if (oCol.id.indexOf("col") == 0)
			{
				oDiv = XMLParser.getNodeFrom(oCol, "div");
				var px = oDiv.offsetWidth;
				rgColInfo[nColumnsFound].pxWidth = px;
				s += px + ",";
				nColumnsFound++;
			}
		}
		
		if ( s.length > 0 )
			s = s.slice(0, -1);
		return s;
	}
	
	// resizeColumn
	//
	// Resize column (ixColToResize) to pxWidth
	//
	this.resizeColumn = function(pxWidth)
	{
		if (ixColToResize == null) return;
		
		var ix;
		for (ix = 0; ix < rgGridData.length; ix++)
		{
			this.resizeColumnInRow(ix, false, pxWidth);
		}
		for (ix = 0; ix < rgHeaderBarCache.length; ix++)
		{
			this.resizeColumnInRow(ix, true, pxWidth);
		}
		
		this.refreshTableCoords();
		ListControl.extendNavBarSpacer(this.getTableRight());
		// IE drawing bug - force redraw or rowspacer won't be resized until an onmouseover event
		redraw(oTable);
	}
	
	this.resizeColumnInRow = function(ixRow, bHeaderBar, pxWidth)
	{
		var oRow = bHeaderBar ? rgHeaderBarCache[ixRow] : this.getRowNode(ixRow);
		
		var oCol;
		if (bHeaderBar)
			oCol = XMLParser.getNodeUsingIdFrom(oRow, "th", "colHead_"+ixColToResize);
		else
			oCol = XMLParser.getNodeUsingIdFrom(oRow, "td", "col_"+ixColToResize);
		
		var oObj = XMLParser.getNodeFrom(oCol, "div");
		
		oObj.style.width = pxWidth + "px";
		oCol.style.width = pxWidth + "px";
	}
	
	this.getHeaderCursor = function(oColHeader)
	{
		if( this.mouseInResizeHotspot(oColHeader) )
			return 'e-resize';
		else
			return 'move';
	}
	
	this.startDragOrResize = function(oColHeader)
	{
		if( this.mouseInResizeHotspot(oColHeader) )
			this.startColumnResize(oColHeader.id);
		else
			this.startColumnDrag(oColHeader);
	}
	
	this.mouseInResizeHotspot = function(oColHeader)
	{
		return (xMouse > (calculateOffset(oColHeader, 'offsetLeft') + oColHeader.offsetWidth - 12));
	}
	
	// autoResize
	//
	// If double clicking the resize hotspot,
	// automatically resize column to width of 
	// longest content
	//
	this.autoResize = function(oColHeader)
	{
		if ( !this.mouseInResizeHotspot(oColHeader) ) return;
		ixColToResize = mapIdToCol[oColHeader.id];
		if (ixColToResize == null) return;
		Info.show(Lang.getString("FB_RESIZING"), null, null);
		setTimeout("GridControl.doAutoResize()",1);
	}
	
	this.doAutoResize = function()
	{	
		if (!bShallowWalkDone)
		{
			// Wait for grid walk to finish building internal model
			setTimeout("GridControl.doAutoResize()", 500);
			return;
		}
		var w = Math.min(this.getMaxContentWidth(ixColToResize), MAX_COL_AUTO_WIDTH);
		this.resizeColumn(w);
		this.sendResizeColumnRequest();
		this.refreshColumnHotSpots();
		removeTextSelections();
		Info.hide();
		ListControl.updateSignature(false);
	}
	
	this.getMaxContentWidth = function(ixColumn)
	{
		var w = MIN_COL_WIDTH;
		var oRow, oCol, oNoBr, oSpan;
		
		var ix;
		for (ix = 0; ix < rgGridData.length; ix++)
		{
			oRow = this.getRowNode(ix);
			oCol = XMLParser.getNodeUsingIdFrom(oRow, "td", "col_"+ixColToResize);
			oNoBr = XMLParser.getNodeFrom(oCol, "nobr");
			oSpan = XMLParser.getNodeFrom(oNoBr, "span");
			if (oSpan.offsetWidth > w) w = oSpan.offsetWidth;
		}
		return w;
	}	
	
} // end GridControl singleton

var SelectionManager = new function()
{
	var bShiftDown = false;
	var bDragging = false;
	var bLastClickSelected = false;
	var ixRowDragCurr = -1;
	var ixRowDragFirst = -1;
	var ixRowDragLast = -1;
	var ixRowDragMax = -1;
	var ixRowDragMin = -1;
	var ixRowShiftMax = -1;
	var ixRowShiftMin = -1;
	var ixLastBugClicked = -1;
	var bLastClickFromCheckbox = false;
	
	var rgbSelected =			"rgb(255,255,179)";
	var rgbSelectedAlt =		"rgb(227,239,167)";
	var rgbUnselected =			"rgb(255,255,255)";
	var rgbUnselectedAlt =		"rgb(214,232,238)";
	var rgbHoverSelected =		"rgb(214,214,171)";
	var rgbHoverSelectedAlt =	"rgb(197,204,163)";
	var rgbHoverUnselected =	"rgb(214,214,214)";
	var rgbHoverUnselectedAlt =	"rgb(201,201,201)";

	this.reset = function()
	{
		// Reset globals on unload since Opera and Safari may cache the script
		// and won't run the initialization code when using back button to return
		// to previous pages
		bShiftDown = false;
		bUserTyped = false;
		bDragging = false;
		bLastClickSelected = false;
		ixRowDragCurr = -1;
		ixRowDragFirst = -1;
		ixRowDragLast = -1;
		ixRowDragMax = -1;
		ixRowDragMin = -1;

		// Store all bug values on page in form field so that a refresh
		// which preserves form state (FF) can compare previous/current
		// ixBug's.  If they don't match, we need to wipe out any
		// persisted checkbox state
		if (document.formAllBugs && document.formAllBugs.ixAllBugs)
		{
			setBugValues(true, document.formAllBugs.ixAllBugs);
		}
	}

	// isIxBugConflict
	//
	// Return true iff the previously viewed list contained a different set of bugs than
	// the current list AND the previous list's form state has been persisted, indicating
	// a refresh (FF persists form state).
	//
	this.isIxBugConflict = function()
	{
		if (document.formAllBugs && document.formAllBugs.ixAllBugs && (document.formAllBugs.ixAllBugs.value != null))
		{
			var sOld = document.formAllBugs.ixAllBugs.value;
			if (sOld.length == 0) return false;
			if (getBugValues(true) != sOld) return true;
		}
		return false;
	}
	
	this.isDragging = function()
	{
		return bDragging;
	}
	
	// Temporarily set row's background color to yield
	// mouseover effect
	// 
	this.tempHighlight = function( ix )
	{
		var oRow = GridControl.getRowNode(ix);
		var oBox = document.bulkForm['ixBulkBug'][ix];
		if (!oBox && ix == 0) oBox = document.bulkForm['ixBulkBug'];
		if (!oRow || !oBox) return;
		
		this.doTempHighlight(oRow, oBox);
	}
	
	this.doTempHighlight = function( oRow, oBox )
	{
		if (!oRow) return;
		this.setRowAltClass(oRow);
		
		var sColor;
		if (oBox && oBox.checked)
			sColor = oRow.fAltClass ? rgbHoverSelectedAlt : rgbHoverSelected;
		else
			sColor = oRow.fAltClass ? rgbHoverUnselectedAlt : rgbHoverUnselected;
		
		oRow.bgCurrent = sColor;
		paintRow(oRow, sColor);
		
		this.setInfoColor(oRow, "black");
	}
	
	this.removeTempHighlight = function( oRow, oBox )
	{
		if (!oRow) return;
		paintRow(oRow, this.getRowColor(oRow, oBox));
		this.setInfoColor(oRow, "rgb(151,151,151)");
	}
	
	this.setRowAltClass = function( oRow )
	{
		if (oRow.fAltClass == null)
		{
			oRow.fAltClass = (oRow.className.indexOf('r-a') != -1);
		}
	}
	
	this.setInfoColor = function(oRow, sColor)
	{
		var rgInfo = XMLParser.getNodeArrayUsingIdFrom(oRow, "span", "xInfo");
		for (var ix = 0; ix < rgInfo.length; ix++)
		{
			rgInfo[ix].style.color = sColor;
		}
	}

	// Set row's background color according to the state
	// of the row's checkbox (selected = highlighted)
	//
	// Return reference to row node
	//
	this.highlightRow = function( ix )
	{
		var rgCheckbox = document.bulkForm['ixBulkBug'];
		if (!rgCheckbox || !GridControl) return null;

		var oRow = GridControl.getRowNode(ix);
		if (!oRow) return null;
		var oBox = 	rgCheckbox.length ? rgCheckbox[ix] : rgCheckbox;
		if (!oBox) return null;
		var rgTD, i, max;
		this.setRowAltClass(oRow);
		
		var sColor = this.getRowColor(oRow, oBox);
		
		if (oRow.bgCurrent != sColor)
		{
			oRow.bgCurrent = sColor
			paintRow(oRow, sColor);
		}
		
		return oRow;
	}
	
	this.getRowColor = function( oRow, oBox )
	{
		if ( oBox.checked )
			return (oRow.fAltClass ? rgbSelectedAlt : rgbSelected);
		else
			return (oRow.fAltClass ? rgbUnselectedAlt : rgbUnselected);
	}

	this.isChecked = function( ix )
	{
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{
			if (bf.length)
			{
				return	bf[ix].checked;
			}
		}
		return false;
	}

	this.gridClick = function( ix, shiftKey, ctrlKey )
	{
		if (bDragging)
		{
			if (this.doClickFromDrag(ix))
			{
				this.doGridClick(ix, false, false);
			}
		}
		else
		{
			this.doGridClick(ix, shiftKey, ctrlKey);
			bLastClickSelected = this.isChecked(ix);
		}
	}

	this.lastClickFromCheckbox = function( b )
	{
		if (b != null) bLastClickFromCheckbox = b;
		return bLastClickFromCheckbox;
	}

	this.doGridClick = function( ix, shiftKey, ctrlKey )
	{
		if (bLastClickFromCheckbox)
		{
			ctrlKey = true;
			bLastClickFromCheckbox = false;
		}
		if (ctrlKey)	this.toggleCheckbox(ix);
		else			this.setCheckbox(ix, true);
		this.highlightRow(ix);
		if (!bDragging)
		{
			if (!ctrlKey && !shiftKey)
			{
				this.uncheckOthers(ix);
				this.uncheckOtherGroupboxes(-1);
			}
			this.checkboxClick(ix, shiftKey);
			if (!this.isChecked(ix))
				SelectionManager.tempHighlight(ix);
			else
				this.setInfoColor(GridControl.getRowNode(ix), "rgb(151,151,151)");
			// This might be the start of a drag-selection
			if (ixRowDragFirst < 0)
				ixRowDragFirst = ix;
			setTimeout(updateBulkActions,1);
		}
		removeTextSelections();
	}

	// doClickFromDrag
	//
	// Returns true if UI should simulate a grid click
	// due to drag-selecting over Grid View.
	//
	// Fixes any missed rows caused by a quick click'n'drag
	// that the browser's mouseover events may have missed
	//
	this.doClickFromDrag = function( ix )
	{
		if (ix == ixRowDragCurr) return false;
		
		if (ixRowDragLast < 0)
		{
			ixRowDragLast = ixRowDragFirst;
		}
		else
		{
			ixRowDragLast = ixRowDragCurr;
		}
		ixRowDragCurr = ix;
		
		// Keep track of the bounds of current drag selection so
		// we can correct spots where browser events didn't fire
		if (ixRowDragCurr < ixRowDragMin || ixRowDragMin < 0)
		{
			ixRowDragMin = ixRowDragCurr;
		}
		if (ixRowDragCurr > ixRowDragMax)
		{
			ixRowDragMax = ixRowDragCurr;
		}
		
		this.doRepairDragHighlights();
		
		if (ix == ixRowDragFirst)
		{
			// don't simulate a grid click on the first-clicked row during drag
			return false;
		}
		
		return (this.isChecked(ix) != bLastClickSelected);
	}

	// doRepairDragHighlights
	//
	// Fix any missed rows caused by a quick click'n'drag
	// that the browser's mouseover events may have missed
	//
	this.doRepairDragHighlights = function()
	{
		if (!bDragging) return;
		
		var i, min, max;
		
		if (ixRowDragFirst < ixRowDragCurr)
		{
			for (i = ixRowDragFirst; i < ixRowDragCurr; i++)
			{
				if (this.isChecked(i) != bLastClickSelected)
				{
					this.setCheckbox(i,bLastClickSelected);
					this.highlightRow(i);
				}
			}
		}
		else if (ixRowDragFirst > ixRowDragCurr)
		{
			for (i = ixRowDragFirst; i > ixRowDragCurr; i--)
			{
				if (this.isChecked(i) != bLastClickSelected)
				{
					this.setCheckbox(i,bLastClickSelected);
					this.highlightRow(i);
				}
			}
		}
		
		if (ixRowDragMin > -1)
		{
			min = Math.min(ixRowDragFirst, ixRowDragCurr);
			for (i = ixRowDragMin; i < min; i++)
			{
				if (this.isChecked(i))
				{
					this.setCheckbox(i,false);
					this.highlightRow(i);
				}
			}
		}
		
		if (ixRowDragMax > -1)
		{
			max = Math.max(ixRowDragFirst, ixRowDragCurr);
			for (i = ixRowDragMax; i > max; i--)
			{
				if (this.isChecked(i))
				{
					this.setCheckbox(i,false);
					this.highlightRow(i);
				}
			}	
		}
	}

	this.doSelectGroup = function( sId, fSelect, ctrlKey )
	{
		if (!sId.indexOf("_")) return;
		var ixGroup, oGroup;
		if (sId.substring(sId.indexOf("_")+1) == "All")
		{
			ixGroup = 1;
			oGroup = elById("group_0");
		}
		else
		{
			ixGroup = sId.substring(sId.indexOf("_")+1);
			if (!ixGroup) return;
			oGroup = elById("group_"+ixGroup);
		}
		
		if (oGroup)
		{
			if (!ctrlKey)
			{
				this.uncheckOthers(-1);
				this.uncheckOtherGroupboxes(ixGroup-1);
			}
			var rgRows = oGroup.getElementsByTagName("tr");
			if (rgRows)
			{
				var ix;
				var ixStart = -1;
				for(var i = 0; i < rgRows.length; i++)
				{
					sId = rgRows[i].getAttribute("id");
					if (sId)
					{
						ix = sId.substring(sId.indexOf("_")+1);
						this.setCheckbox(ix, fSelect);
						if (ixStart == -1) ixStart = ix;
					}
				}
				this.highlightAll(ixStart, ix);
			}
		}
		setTimeout(updateBulkActions, 1);
	}

	this.gridGrouperClick = function( sId, ctrlKey )
	{
		if (!sId.indexOf("_")) return;
		removeTextSelections();
		var o;
		if (sId == "groupAnchor_All")
		{
			o = elById("groupBox_All");
		}
		else
		{
			var ixGroup = sId.substring(sId.indexOf("_")+1);
			if (!ixGroup) return;
			o = elById("groupBox_"+ixGroup);
		}
		if (o)
		{
			o.checked = !o.checked;
			this.doSelectGroup(sId, o.checked, ctrlKey);
		}
	}

	this.checkboxDown = function( keycode )
	{	
		if (keycode == 16) bShiftDown = true;
	}

	this.checkboxUp = function( e, keycode )
	{
		if (!e) e = window.event;
		
		var nKey;
		if ( keycode >= 0 )
		{
			nKey = keycode;
		}
		else if (e.which >= 0)
		{
			nKey = e.which
		}
		else
		{
			nKey = e.keyCode
		}
		if (nKey == 16) bShiftDown = false;
	}

	this.checkboxClick = function( ix, shiftKey )
	{
		if (bShiftDown || shiftKey)
		{
			if (ix > ixRowShiftMax)
				ixRowShiftMax = ix;
			if (ix < ixRowShiftMin || ixRowShiftMin < 0)
				ixRowShiftMin = ix;
			var begin = -1
			var end = -1
			if (ixLastBugClicked > ix)
			{
				begin = ix
				end = ixLastBugClicked
			}
			else
			{
				begin = ixLastBugClicked
				end = ix
			}
			if (ixRowShiftMax > end)
				this.checkAll(end+1, ixRowShiftMax+1, false);
			if (ixRowShiftMin < begin)
				this.checkAll(ixRowShiftMin, begin, false);
			if (begin != end)
				this.checkAll(begin, end, true)
		}
		else
		{
			ixLastBugClicked = ix;
			ixRowShiftMax = -1;
			ixRowShiftMin = -1;
		}
	}

	// uncheckOthers
	//
	// Uncheck all row checkboxes except ix, 
	//
	this.uncheckOthers = function( ix )
	{
		var i = 0;
		if (!document.bulkForm) return;
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{ 
			if (bf.length)
			{
				for (i=0; i<bf.length; i++)
				{
					if (i != ix && bf[i].checked)
					{
						bf[i].checked = false;
						this.highlightRow(i);
					}
				}
			}
			else
			{
				if (ix != 0 && bf.checked)
				{
					bf.checked = false;
					this.highlightRow(0);
				}
			}
		}
	}

	this.uncheckOtherGroupboxes = function(ix)
	{
		var bf = document.bulkForm['ixBulkBugAll'];
		if (bf)
		{ 
			if (bf.length)
			{
				for (i=0; i<bf.length; i++)
				{
					if (i != ix && bf[i].checked)
					{
						bf[i].checked = false;
					}
				}
			}
			else
			{
				if (ix != 0 && bf.checked)
				{
					bf.checked = false;
				}
			}
		}
	}

	// Checks (or unchecks) and highlights all rows within 
	// a specified range
	//
	this.checkAll = function( begin, end, bChecking )
	{
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{
			if (bf.length)
			{
				for( var i = begin; i < end; i++)
				{
					bf[i].checked = bChecking;
				}

				updateBulkActions();
				this.highlightAll(begin,end);
			}
		}
	}

	// Highlights all rows within 
	// a specified range.
	//
	// Rows will be un-highlighted automatically depending
	// on checkbox status.
	//
	this.highlightAll = function( begin, end )
	{
		// only redraw 50 (at max) rows at a time before returning control to user
		var min = Math.min(begin+50, end);
		for( var i = begin; i <= min; i++)
		{
			this.highlightRow(i);
		}
		
		// if necessary, finishing highlighting rest asynchronously
		if (end > min)
		{
			setTimeout('SelectionManager.highlightAll('+min+','+end+')',1);
		}
	}

	this.toggleCheckbox = function( ix )
	{
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{
			if (bf.length)
			{
				bf[ix].checked = !bf[ix].checked;
			}
			else
			{
				bf.checked = !bf.checked;
			}
		}
	}

	this.setCheckbox = function( ix, fChecked )
	{
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{
			if (bf.length)
			{
				if (bf[ix])	bf[ix].checked = fChecked;
			}
			else
			{
				if (bf) bf.checked = fChecked;
			}
		}
	}

	this.startDragging = function()
	{
		bDragging = true;
	}

	this.stopDragging = function()
	{
		bLastClickSelected = false;
		ixRowDragCurr = -1;
		ixRowDragFirst = -1;
		ixRowDragLast = -1;
		ixRowDragMax = -1;
		ixRowDragMin = -1;
		if (bDragging)	updateBulkActions();
		bDragging = false;
	}
		
	this.doSelectAll = function()
	{
		var i = 0;
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{ 
			if (bf.length)
			{
				for (i=0; i<bf.length; i++)
				{
					bf[i].checked = true;
				}
				this.highlightAll(0,bf.length);
			}
			else
			{
				bf.checked = true;
				this.highlightRow(0);
			}
			updateBulkActions();
		}
	}

	this.doUnselectAll = function()
	{
		var i = 0;
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{
			if (bf.length)
			{
				for (i=0; i<bf.length; i++)
				{
					bf[i].checked = false;
				}
				this.highlightAll(0,bf.length);
			}
			else
			{
				bf.checked = false;
				this.highlightRow(0);
			}
			updateBulkActions();
		}
		var bfa = document.bulkForm['ixBulkBugAll'];
		if (bfa)
		{
			if (bfa.length)
			{
				for (i=0; i<bfa.length; i++)
				{
					bfa[i].checked = false;
				}
			}
			else
			{
				bfa.checked = false;
			}
			updateBulkActions();
		}
	}
	
	this.doSelectChecked = function()
	{
		var i = 0;
		if (!document.bulkForm) return;
		var bf = document.bulkForm['ixBulkBug'];
		if (bf)
		{ 
			if (bf.length)
			{
				for (i=0; i<bf.length; i++)
				{
					if (bf[i].checked)
						this.highlightRow(i);
				}
			}
			else
			{
				if (bf.checked)
					this.highlightRow(0);
			}
		}
	}
	
	var SCROLL_STEP = 20; // pixels to scroll each step
	var SCROLL_WAIT = 50; // ms wait between each scroll step
	var SCROLL_THRESHOLD = 100; // pixel boundary on top/bottom of window for scroll hotspots
	
	// shouldScrollDown - true if mouse is in window's scroll down hotspot
	this.shouldScrollDown = function(y)
	{
		return bDragging && (y > (scrollTop() + windowHeight() - SCROLL_THRESHOLD));
	}
	
	// shouldScrollUp - true if mouse is in window's scroll up hotspot
	this.shouldScrollUp = function(y)
	{
		return bDragging && (y < (scrollTop() + SCROLL_THRESHOLD));
	}
	
	// scrollDown
	//
	// Scroll down by SCROLL_STEP, and continue
	// as necessary until user moves mouse or
	// bottom of page is reached
	//
	this.scrollDown = function(y, yId)
	{
		if (yMouse != yId) return;
		var yStart = scrollTop();
		window.scrollBy(0, SCROLL_STEP);
		y += (scrollTop()-yStart);
		if (y != yStart)
			setTimeout("SelectionManager.fixScrollWindow("+y+","+yId+")",SCROLL_WAIT);
	}
	
	// scrollUp
	//
	// Scroll up by SCROLL_STEP, and continue
	// as necessary until user moves mouse or
	// top of page is reached
	//
	this.scrollUp = function(y, yId)
	{
		if (yMouse != yId) return;
		var yStart = scrollTop();
		window.scrollBy(0, -1 * SCROLL_STEP);
		y -= (yStart-scrollTop());
		if (y != yStart)
			setTimeout("SelectionManager.fixScrollWindow("+y+","+yId+")",SCROLL_WAIT);
	}
	
	this.fixScrollWindow = function(y, yId)
	{	
		if (this.shouldScrollDown(y))
		{
			this.scrollDown(y, yId);
		}
		else if (this.shouldScrollUp(y))
		{
			this.scrollUp(y, yId);
		}
	}
	
	this.notifyMovement = function()
	{
		if (bDragging) this.fixScrollWindow(yMouse, yMouse);
	}

} // end SelectionManager singleton
