/**
  *  Gradebook data grid
  *
  *  PORTIONS OF THIS FILE ARE BASED ON RICO LIVEGRID 1.1.2
  *
  *  Copyright 2005 Sabre Airline Solutions
  *
  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
  *  file except in compliance with the License. You may obtain a copy of the License at
  *
  *         http://www.apache.org/licenses/LICENSE-2.0
  *
  *  Unless required by applicable law or agreed to in writing, software distributed under the
  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  *  either express or implied. See the License for the specific language governing permissions
  *  and limitations under the License.
  *
  * File: $Workfile: $
  * Authors(s): Bill Richard
  * Description: Main controller class for gradebook2 grid.
  * Version: $Revision: $ $Date: $
  **/

var Gradebook = {
  Version: '1.0.0',
  prototypeVersion: parseFloat(Prototype.Version.split(".")[0] + "." + Prototype.Version.split(".")[1]),
  getModel: function() {
    try {
    	if (window.gbModel) return window.gbModel; // in case scope is GC/Course Frameset
    	if (parent.gbModel) return parent.gbModel; 
    	return parent.parent.gbModel;
    } catch (ignore) {
        return null;
    } 
  },
  clearModel: function() {parent.gbModel = null; }
}


Gradebook.Grid = Class.create();

Gradebook.Grid.prototype = {

   initialize: function( tableId, gradebookService, options, model) {

     this.options = {
			scrollerBorderRight: '1px solid #ababab',
			sortBlankImg: 'images/blank.gif',
			sortAscendImg: '/images/ci/icons/sort_on_up.gif',
			sortDescendImg: '/images/ci/icons/sort_on.gif',
			topArrowLImg: 'images/toparrowL.gif',
			topArrowRImg: 'images/toparrowR.gif',
			botArrowLImg: 'images/botarrowL.gif',
			botArrowRImg: 'images/botarrowR.gif',
			numFrozenColumns: 0,
			accessibleMode: false
                };
		Object.extend(this.options, options || {});

		this.tableId     = tableId; 
		this.table       = $(tableId);

		this.currentSelectedCell = null;

		if (model){
			this.model = model;
  			this.model.removeModelListeners();
  			this.model.gradebookService = gradebookService;
  		} else {
			this.model = new Gradebook.GridModel(gradebookService);
  		}
		this.model.addModelListener(this);
		this.initClearAttemptsFlyOut();
		if (this.model.getNumColDefs() == 0)
			this.model.requestLoadData();
		else
			this.model.requestUpdateData();

            this.moveToRoot('gradeCM');
            this.moveToRoot('gradeHeaderCM');
            this.moveToRoot('studentInfoHeaderCM');
            this.moveToRoot('studentInfoCM');
   },

    // move the element to the root level of the doc
    moveToRoot: function( eName ) {
      var b = document.getElementsByTagName('body')[0];
      var eObj = $( eName );
	eObj.remove();
	b.appendChild( eObj );
    },


	modelChanged: function() {
		$('loadStatusMsg').update(GradebookUtil.getMessage('creatingGridMsg'));
		this.model.removeModelListeners();
		setTimeout(this.createView.bind(this), 50 );
	},

	modelError: function(exception, serverReply) {
		model = null;
		parent.gbModel = null;
		if (serverReply && !serverReply.startsWith('{"dataFormat":')){
			// server returned error page instead of json data
			document.write( serverReply );
			document.close();
		} else {
		  $('loadstatus').hide();
		  $('loadingGridErrorMsg').update( GradebookUtil.getMessage('errorParsingDataMsg') );
		  $('errorLoadingGrid').show();
		  if ( exception )
		  {
		    if ( exception.name && exception.message ) 
		    {
		      $('loadingGridError').update( exception.name + ': ' + exception.message );
		    }
		    else
		    {
		      $('loadingGridError').update( exception );
		    }
		  }
		}
	},
	
	initClearAttemptsFlyOut: function()
	{
	   var clearAttempsFormPanel = $('clearAttemptsFlyOut');
	  // direct root child to solve absolute positioning issues
	  //clearAttempsFormPanel.remove();
	  //document.getElementsByTagName('body')[0].appendChild( clearAttempsFormPanel );
	  Event.observe(clearAttempsFormPanel, 'click', function( event ) { Gradebook.doNotCloseAttemptsForm = true; } );
	  Event.observe($('dp_bbDateTimePicker_start_date'), 'click', function( event ) { $('clearAttemptsOptionRange').checked = true; } );
	  Event.observe($('dp_bbDateTimePicker_end_date'),   'click', function( event ) { $('clearAttemptsOptionRange').checked = true; } );
	  Event.observe($('dp_bbDateTimePicker_start_date'), 'change', function( event ) { $('clearAttemptsOptionRange').checked = true; } );
	  Event.observe($('dp_bbDateTimePicker_end_date'),   'change', function( event ) { $('clearAttemptsOptionRange').checked = true; } );
	  Event.observe('selectOption', 'change', function( event ) { $('clearAttemptsOptionSelect').checked = true; } );
	  Gradebook.clearAttemptsFormDefault = new Object();
	  Gradebook.clearAttemptsFormDefault.defaultSelect = $('selectOption').value;
	  Gradebook.clearAttemptsFormDefault.defaultStartDate = $('dp_bbDateTimePicker_start_date').value;
	  Gradebook.clearAttemptsFormDefault.defaultEndDate = $('dp_bbDateTimePicker_end_date').value;
	  Gradebook.clearAttemptsFormDefault.defaultStartDateHidden = $('bbDateTimePickerstart').value ;
    Gradebook.clearAttemptsFormDefault.defaultEndDateHidden =$('bbDateTimePickerend').value ;
	  
	  Event.observe('clearAttemptsFlyOutCancel', 'click', function( event ) { $("clearAttemptsFlyOut").style.display = "none"; } );
	},

	createView: function() {
		this.options.numFrozenColumns = model.getNumFrozenColumns();
		this.modelSortIndex = this.model.getSortIndex();
		this.sortDir = this.model.getSortDir();
		this._initializeHTML();
		this.viewPort =  new Gradebook.GridViewPort(this.table, this.model, this.options, this);
		this.model.addModelListener(this.viewPort);
		if ( this.options.onLoadComplete ){
			this.options.onLoadComplete();
		}
		this.viewPort.refreshContentsH();
		this._setAccessibilityHeaders();
		this.updateSortImage();
		this.restoreFocus();
	},

	setAccessibleViewportSize: function( ) {
		// container div will scroll in accessible mode
		var contDiv = $(this.tableId+'_container');
		var oneRowHeight = 20;
		if ( this.table.rows.length > 0 )
		{
		  oneRowHeight = this.table.rows[this.table.rows.length-1].offsetHeight + 1 /*border spacing*/;
		}
		//need to account for the wider header row
		var h = Math.max(this.options.tableHeight, ( ( this.model.getMinimumRows() + 1 ) * oneRowHeight + 10 ) );
		h = Math.min( h, this.table.offsetHeight+19 );
		var w = Math.min(this.options.tableWidth, this.table.offsetWidth+19);
		contDiv.style.height = h + "px";
		contDiv.style.width = w + "px";
		contDiv.style.overflow = "auto";
	},

	_initializeHTML: function() {
		this._sizeHTMLTable();

		// wrap table with a new container div: relative see IE7 bug: http://rowanw.com/bugs/overflow_relative.htm
		this.table.insert({before: "<div id='"+this.tableId+"_container' style='position:relative;'></div>"});
		this.table.previousSibling.appendChild(this.table);

		if (this.options.accessibleMode)
		{
			this.setAccessibleViewportSize( );
			return;
		}
		// wrap table with a new viewport div
		this.table.insert({before: "<div id='"+this.tableId+"_viewport'></div>"});
		this.table.previousSibling.appendChild(this.table);
		var viewportDiv = $(this.tableId+'_viewport');
		viewportDiv.style.height = (this.table.offsetHeight) + "px";
		viewportDiv.style.overflow = "hidden";
		
		// add controllers to table cells
		var tableHeader = $(this.table.id + '_header');
		if (tableHeader)
		{
        	var numHCols = tableHeader.rows[0].cells.length;
        	for (var c = 0; c < numHCols; c++)
        	{
          		new Gradebook.CellController(tableHeader.rows[0].cells[c], this, 0, c, true);
        	}
      	}
     	var numRows = this.table.rows.length;
		for (var r = 0; r < numRows; r++)
		{
			var numCols = this.table.rows[0].cells.length;
			for (var c = 0; c < numCols; c++)
			{
				var cell = this.table.rows[r].cells[c];
				new Gradebook.CellController(cell, this, r, c, false);
			}
		}

		if (document.onClickHandler){
			Event.stopObserving(document,'click',document.onClickHandler);
		}
		document.onClickHandler = this.onDocumentClickHandler.bindAsEventListener(this);
		Event.observe(document,'click',document.onClickHandler);
		
		if (document.onKeydownHandler){
			Event.stopObserving(document,'keydown',document.onKeydownHandler);
		}
		document.onKeydownHandler = this.onDocumentKeyDownHandler.bindAsEventListener(this);
		Event.observe(document,'keydown',document.onKeydownHandler);
	},

	unload: function() {
		Gradebook.CellController.prototype.removeAllSavingDivs();
		var numRows = this.table.rows.length;
		for (var r = 0; r < numRows; r++){
			var numCols = this.table.rows[0].cells.length;
			for (var c = 0; c < numCols; c++){
				var cell = this.table.rows[r].cells[c];
				if (cell.controller)
				{
					cell.controller.unload();
				}
			}
		}
		var tableHeader = $(this.table.id + '_header');
		if (tableHeader){
			var numHCols = tableHeader.rows[0].cells.length;
			for (var c = 0; c < numHCols; c++){
				var cell = tableHeader.rows[0].cells[c];
				if (cell.controller)
				{
					cell.controller.unload();
				}
			}
		}
		if (this.viewPort) this.viewPort.unload();
		this.model.removeModelListeners();
		this.table = null;
		this.model = null;
		this.viewPort = null;
		this.options = null;
		this.sortCell = null;
	},

	_sizeHTMLTable: function() {
		var tbl = this.table;
		var tableHeader = $(this.table.id + '_header');
		var numRows = 0;
		var numCols = 0;  
		var numFrozenColumns = this.options.numFrozenColumns;
		// presence of th impacts the calculation of the row height
		// so we remove it before the calculation occurs
		if (numFrozenColumns == 0){
			for (var i=0; i<tbl.rows.length; i++) {
				tbl.rows[i].deleteCell(1);
				tableHeader.rows[i].deleteCell(1);
			}
			// region is now too small to display msg and count
			$("selectedRowMsg").style.display='none';
		}
		else
		{
			$("selectedRowMsg").style.display='inline';
		}		
		if (this.options.accessibleMode){
			numRows = this.model.getNumRows();
			numCols = this.model.getNumColDefs();  
		} else {
			var cell = this.table.rows[this.table.rows.length-1].cells[1]; // skip checkbox column
			cell.height = cell.offsetHeight;
			numRows = parseInt(this.options.tableHeight/cell.offsetHeight);
			if ( numRows < ( this.model.getMinimumRows() + 1 ) ) numRows = ( this.model.getMinimumRows() + 1 );
			numCols = parseInt(this.options.tableWidth/cell.offsetWidth);
		}
		if (this.model.getNumRows() <= numRows) {
			numRows = this.model.getNumRows() + 1; // add one row for header
			if (this.model.getNumRows() == 0){
				numRows = 0;
			}
		}

		// at least one non-frozen column must be shown
		if (numFrozenColumns+1 >= numCols){
			numFrozenColumns = numCols-1;
			this.options.numFrozenColumns = numFrozenColumns;
		}
		
		// assumes the table has at least 1 row & 2 cols
		// the first column is a frozen column
		// the second column is a non-frozen column

		// clone frozen columns
		for (var i = 0; i < numFrozenColumns-1; i++){
			this._cloneColumn(1); // skip check box column
		}


		// clone non-frozen columns
		var numNonFrozenColumns = numCols - numFrozenColumns - 1;
		for (var i = 0; i < numNonFrozenColumns; i++){
			this._cloneColumn(numFrozenColumns+1); // skip check box column
		}

		
	    var checkColumnWidth = this.table.rows[0].cells[0].offsetWidth;
	    var visibleWidth = this.table.offsetWidth;
	    this.avgColWidth = (visibleWidth - checkColumnWidth)/numCols;
	    var frozenWidth = (numFrozenColumns * this.avgColWidth) + checkColumnWidth;
        $("selectedRows").style.width=this.isIE?frozenWidth + "px": frozenWidth-2 +"px";
		
		
		// clone rows
		var numRowsToAdd = numRows - tbl.rows.length;
		if (tableHeader) numRowsToAdd--;
		
		var rowToClone = tbl.rows[this.table.rows.length-1];
		for (var i = 0; i < numRowsToAdd; i++){
			tbl.tBodies[0].appendChild(rowToClone.cloneNode(true));
		}

		// remove table rows if html table is bigger than numRows
		while (tbl.rows.length > numRows){
			if (tbl.rows.length > 0) tbl.deleteRow(tbl.rows.length - 1);
		}

		// remove table columns if html table is bigger than model
		var allRows = tbl.rows;	
		while (tbl.rows.length > 0 && tbl.rows[0].cells.length-1 > this.model.getNumColDefs()){
			for (var i=0; i<allRows.length; i++) {
				if (allRows[i].cells.length > 1) {
					allRows[i].deleteCell(-1);
				}
			}
		}
		while (tableHeader && tableHeader.rows[0].cells.length-1 > this.model.getNumColDefs()){
			tableHeader.rows[0].deleteCell(-1);
		}
	},

	_cloneColumn: function(colIndex){
		var tbl = this.table;
		for (var i = 0; i < tbl.rows.length; i++) {
			var origCell = tbl.rows[i].cells[colIndex]; 
			var newCell = origCell.cloneNode(true);
			tbl.rows[i].insertBefore(newCell,origCell);
		}
		var tableHeader = $(this.table.id + '_header');
		if (tableHeader){
			var tbl = tableHeader;
			for (var i = 0; i < tbl.rows.length; i++) {
				var origCell = tbl.rows[i].cells[colIndex]; 
				var newCell = origCell.cloneNode(true);
				tbl.rows[i].insertBefore(newCell,origCell);
			}
		}
	},      

	_setAccessibilityHeaders: function(){
		if (!this.options.accessibleMode){
			return;
		}
		var tbl = this.table;

		// add abbr attributes to all header cells
		var hdrRow = tbl.rows[0];
	    for ( var i = 1; i < hdrRow.cells.length; i++ ) {
	      var cell = hdrRow.cells[i];
	      cell.abbr = cell.controller.getGridCell().getName();
		}
		tbl.rows[0].cells[0].abbr = " ";

		var abbrColIndexs = this.getAbbrColIndexs();
		
		if ( tbl.rows.length > 1 && tbl.rows[1].cells.length > 1 ) 
		{
		  for ( var i = 1; i < tbl.rows.length; i++ ) 
		  {
		    abbrColIndexs.each(function(idx) {
	          var cell = tbl.rows[i].cells[ idx + 1 ]; // skip checkbox cell
	          cell.abbr = cell.controller.getGridCell().getValue();
		      cell.scope = 'row';
		    });
		  }
		}
	},      

	getAbbrColIndexs: function() 
	{
		if ( !this.abbrColIndexs )
		{
			this.abbrColIndexs = new Array();
			/*
			Add abbr attributes to specific columns to allow screen readers to 
			announce meaningful column headers based on the following rules:
			
				1. If both first and last name are visible, use those.
				2. If the username is visible, use that.
				3. If neither of the first cases pass, use the first column as the header. 
			*/
			var lastNameColIndex = this.model.getVisibleColDefIndex('LN');
			var firstNameColIndex = this.model.getVisibleColDefIndex('FN');
			var userNameColIndex = this.model.getVisibleColDefIndex('UN');
			if ( lastNameColIndex != -1 &&  firstNameColIndex != -1 )
			{
			  this.abbrColIndexs.push( lastNameColIndex ); 
			  this.abbrColIndexs.push( firstNameColIndex );
			}
			else if  ( userNameColIndex != -1 )
			{
			  this.abbrColIndexs.push( userNameColIndex );
			}
			else
			{
			  this.abbrColIndexs.push( 0 );
		}
		}
		return this.abbrColIndexs;
	},      

	onDocumentClickHandler: function(evt) {
	  if ( document.ignoreOnClick ) return;
		Gradebook.CellController.prototype.closePopupsAndRestoreFocus(evt);
	},

	onDocumentKeyDownHandler: function(evt) {
		GradebookUtil.debug('onDocumentKeyDownHandler keyCode = '+evt.keyCode);
		if (!Gradebook.CellController.prototype.tableHasFocus) return;
		var ek=evt.keyCode;
		var visibleRowCount = this.viewPort.getNumVisibleRows();
		var deltaRow = 0;
		var deltaCol = 0;
		/*
		 * the model grid cell index is R2L agnostic: thus moving right in L2R is moving towards the next col (+1), 
		 * while in R2L it is going towards the previous col (-1).
		 */
		switch (ek) {
			case (Event.KEY_LEFT):		deltaCol = page.util.isRTL()?1:-1; break;
			case (Event.KEY_RIGHT):		deltaCol = page.util.isRTL()?-1:1; break;
			case (Event.KEY_UP):		deltaRow = -1; break;
			case (Event.KEY_DOWN):		deltaRow = 1; break;
			case (33/* page up */):		if ( !this.options.accessibleMode ) deltaRow = -visibleRowCount; break;
			case (34/* page down */):	if ( !this.options.accessibleMode ) deltaRow = visibleRowCount; break;
			case (Event.KEY_TAB):
			  if ( !Gradebook.CellController.currentSelectedCell ||
			       !Element.descendantOf( evt.element(), Gradebook.CellController.currentSelectedCell.controller.htmlCell ) ) break;
			  if ( evt.shiftKey ) {
			    if ( !evt.element().hasClassName('cmimg') && !this.isFirstCell() )
			      deltaCol = -1; break;
			  } else if ( !this.isLastCell() &&
  			  ( evt.element().hasClassName('cmimg') || this.isCurrentCellWithoutMenu() ) ) {
  			  deltaCol = 1;
  			}
			  break;
		}
		if (deltaRow == 0 && deltaCol == 0) {
			return;
		} else {
			Event.stop( evt );
			this.selectRelativeCell(deltaRow, deltaCol);
			Gradebook.CellController.prototype.closePopups(evt);
		}
	},
	
	isLastCell: function()
	{
	  if ( Gradebook.CellController.currentSelectedCell == null ) return false;
	  // last cell if it is the last displayed cell with no more scroll available right or down
	  var nextSelectedCol = Gradebook.CellController.currentSelectedCell.controller.col;
    var nextSelectedRow = Gradebook.CellController.currentSelectedCell.controller.row+1;

	  return ( nextSelectedCol >= this.viewPort.numVisibleCols ) &&
	         ( nextSelectedRow >= this.viewPort.numVisibleRows ) &&
	         ( ( this.viewPort.lastRowPos/*offset*/ +  this.viewPort.numVisibleRows ) == this.model.getNumRows() ) && 
	         ( ( this.viewPort.colOffset + this.viewPort.numVisibleCols ) == this.model.getNumColDefs() );
	},

  isFirstCell: function()
  {
	  if ( Gradebook.CellController.currentSelectedCell == null ) return false;
	  return ( Gradebook.CellController.currentSelectedCell.controller.col==1/*checkbox*/ &&
	           Gradebook.CellController.currentSelectedCell.controller.row==0 &&
	           ( !this.viewPort.scrollerDiv /*null if no vertical scroll*/ || this.viewPort.scrollerDiv.scrollTop == 0 ) );
  },

	isCurrentCellWithoutMenu: function()
	{
     // the only cell type that does not display a context menu are calculated columns
	   if ( Gradebook.CellController.currentSelectedCell == null ) return false;
	   var gridCell = Gradebook.CellController.currentSelectedCell.controller.getGridCell();
	   if ( !gridCell ) return true;
	   return gridCell.isGrade() && !gridCell.canEdit(); 
	},
		
	selectRelativeCell: function(deltaRow, deltaCol) {
		var visibleRowCount = this.viewPort.getNumVisibleRows();
		var visibleColumnCount = this.viewPort.getNumVisibleCols();
		var modelRowCount = this.model.getNumRows();
		var modelColumnCount = this.model.getNumColDefs();

		var cellController = this.currentCellController;
		if (Gradebook.CellController.currentSelectedCell != null){
			cellController = Gradebook.CellController.currentSelectedCell.controller;
		}
		var currentSelectedRow = cellController.row;
		var currentSelectedCol = cellController.col - 1; // skip checkbox col
		var selectDelay = 100;

		currentSelectedRow += deltaRow;
		if (currentSelectedRow < 0 || currentSelectedRow >= visibleRowCount){
			currentSelectedRow -= deltaRow;
			selectDelay = 500; // need longer delay to select cell until scroll completes
			if (this.viewPort.scrollRows(deltaRow ) == false) {
				if (deltaRow < 0){ 
					// wrap to bottom of previous col
					if (currentSelectedCol == 0) return;
					deltaRow = modelRowCount - visibleRowCount;
					currentSelectedRow = visibleRowCount - 1;
					currentSelectedCol -= 1;
				} else {
					// wrap to top of next col
					deltaRow = visibleRowCount - modelRowCount;
					currentSelectedRow = 0;
					if (currentSelectedCol < visibleColumnCount-1){ 
						currentSelectedCol += 1;
					} else {
						this.viewPort.scrollCols(1);
					} 
				}
				this.viewPort.scrollRows(deltaRow);
			}
		}
		currentSelectedCol += deltaCol;
		if ((currentSelectedCol < this.options.numFrozenColumns && deltaCol < 0)
			|| currentSelectedCol >= visibleColumnCount){
			currentSelectedCol -= deltaCol;
			selectDelay = 500; // need longer delay to select cell until scroll completes
			if (this.viewPort.scrollCols( deltaCol ) == false) {
				if (deltaCol < 0){ 
					if (currentSelectedCol > 0) { // navigate in frozen columns
						currentSelectedCol += deltaCol;
					} else {
						// wrap to end of previous row
						if (currentSelectedRow == 0) return;
						deltaCol = modelColumnCount - visibleColumnCount;
						currentSelectedCol = visibleColumnCount - 1;
						currentSelectedRow -= 1;
					}
				} else {
					// wrap to beginning of next row
					deltaCol = visibleColumnCount - modelColumnCount;
					currentSelectedCol = 0;
					if (currentSelectedRow < visibleRowCount-1){ 
						currentSelectedRow += 1;
					} else {
						this.viewPort.scrollRows(1);
					} 
				}
				this.viewPort.scrollCols(deltaCol);
			}
		}
		// select the current cell after servicing the main event loop to allow current events to complete
		// this was needed for AS-110508 to apply the left/right arrow event to cell navigation only and not to cell editing too.
		this.currentCellController = this.table.rows[currentSelectedRow].cells[currentSelectedCol+1].controller;
		setTimeout(this.selectCell.bind(this), selectDelay );
	},
	
	selectCell: function() {
		this.currentCellController.selectCell();
	},

	sortColumn: function(newSortCell,sortDir) {
		if (newSortCell != this.sortCell) {
			this.sortDir = 'ASC';
			if (this.sortCell) {
				this.sortCell.setSortImage('NO_SORT');	// remove current sort image
			}
		} else {
			this.sortDir = (this.sortDir == 'ASC')?'DESC':'ASC'; // toggle
		}
		if (sortDir){
			this.sortDir = sortDir;
		}
		this.sortCell = newSortCell;
		this.sortCell.setSortImage(this.sortDir);	// show new sort image
		
		// sort the model
		this.modelSortIndex = this.viewPort.toModelIndex(this.sortCell.col-1); // skip checkbox column
		this.model.sort(this.modelSortIndex,this.sortDir);
		
		// refresh the view
     	this.viewPort.moveScroll(0);
		this.viewPort.refreshContents(0);
	},

	updateSortImage: function(){
		if (!this.viewPort){
			return;
		}
		if (this.sortCell) {
			this.sortCell.setSortImage('NO_SORT');	// remove current sort image
		}
		var viewSortIndex = this.viewPort.toViewIndex(this.modelSortIndex);
		if (viewSortIndex < 0){
			this.sortCell = null;
		} else {
			var headerTable = $(this.table.id + '_header');
			if (!headerTable){
				return;
			}
			this.sortCell = headerTable.rows[0].cells[viewSortIndex+1].controller; // add 1 to account for check column			
			this.sortCell.setSortImage(this.sortDir);
		}
	},
	
	//focused is restored only in AX view since user has to leave the page for update
	restoreFocus: function() {
	   if ( !this.options || !this.options.accessibleMode || !Gradebook.getModel().lastFocusedRow || !Gradebook.getModel().lastFocusedCol ) return;
	   if ( GradebookUtil.isIE() ) {
	   	 setTimeout(this.doRestoreFocus.bind(this), 0 );
	   }
	   else
	   {
	     this.doRestoreFocus();
	   }
	},
	
	doRestoreFocus: function() {
	   var lastFocusedRow = Gradebook.getModel().lastFocusedRow;
	   var lastFocusedCol = Gradebook.getModel().lastFocusedCol;
	   this.table.rows[lastFocusedRow].cells[lastFocusedCol].controller.selectCell();
	   Gradebook.getModel().lastFocusedRow = null;
	   Gradebook.getModel().lastFocusedCell = null;
	}
	
};

//Gradebook.GridViewPort --------------------------------------------------
Gradebook.GridViewPort = Class.create();

Gradebook.GridViewPort.prototype = {

	initialize: function(table, model, options,grid) {
		this.isIE = GradebookUtil.isIE();
		this.isNS7 = GradebookUtil.isNS7();
		this.table = table;
		this.model = model;
		this.options = options;
		this.grid = grid;
		this.lastPixelOffset = 0;
		this.colOffset = 0;
		this.lastRowPos = 0;
		this.startScrollLeft = 0;
		this.headerTableId = this.table.id + '_header';
		this.headerTable   = $(this.headerTableId);
		if (!this.headerTable) 
			this.headerTable = this.table;
		this.numVisibleRows = this.table.rows.length;
		if ( this.headerTable.rows[0] )
		{
		  this.numVisibleCols = this.headerTable.rows[0].cells.length-1; // don't include check column
		}
		this.rowHeight = this.table.offsetHeight/this.numVisibleRows;
		this.div = this.table.parentNode;

		this.initScrollers();
	},

	unload: function() {
		this.grid = null;
		this.model = null;
		this.table = null;
		this.headerTable = null;
		this.div = null;
		this.scrollerDiv = null;
		this.heightDiv = null;
		this.scrollerDivH  = null;
		this.widthDiv = null;
		this.options = null;
	},
	
	modelChanged: function() {
		this.refreshContentsH();
	},
	
	getModelGridCell: function(row, col) {
		// in accessibleMode, row 0 is header, so we need to compensate to get model row
		if (this.options.accessibleMode && row > 0){
			row -= 1;
		}
		if (col > 0) col -= 1; // skip check col
		if (col >= this.options.numFrozenColumns) 
			col += this.colOffset; 
		var iterators = this.model.getRowIterators(row+this.lastRowPos, 1, col);
		if (!iterators || iterators.length != 1 || !iterators[0].hasNext()){
			GradebookUtil.error('getModelGridCell cannot get grid cell for row: '+ row+' col: '+col);
		}
		return iterators[0].next();
	},

	getHeaderGridCell: function(col) {
		if (col > 0) col -= 1; // skip check col
		if (col >= this.options.numFrozenColumns) 
			col += this.colOffset; 
		var iterator = this.model.getColDefIterator(col);
		if (!iterator || !iterator.hasNext()){
			GradebookUtil.error('getHeaderGridCell cannot get header cell for col: '+col);
		}
		return iterator.next();
	},

	getNumVisibleRows: function() {
		return this.numVisibleRows;
	},

	getNumVisibleCols: function() {
		return this.numVisibleCols;
	},

	populateRow: function(htmlRow, frozenColRowIterator, scrollableColRowIterator) {
		var numFrozenColumns = this.options.numFrozenColumns;
		for (var j=0; j < (this.numVisibleCols); j++) {
			var iterator = (j < numFrozenColumns)?frozenColRowIterator:scrollableColRowIterator;
			var gridCell = iterator.next();
			var htmlCell = htmlRow.cells[j+1];
			// set check box column based on isRowChecked flag for first grid cell
			if (j == 0){
				var checkInput = GradebookUtil.getChildElementByClassName(htmlRow.cells[0], 'input', 'checkInput');
				checkInput.checked = gridCell.isRowChecked();
			}
			htmlCell.controller.renderHTML(gridCell);
		}
	},
   
	refreshContents: function(rowOffset) {
		if (this.model.getNumRows() == 0) return;
		if (this.options.accessibleMode){
			this.refreshAccessibleContents();
			return;
		}
		Gradebook.CellController.prototype.hideAllSavingDivs();
		var numRows = this.numVisibleRows;
		var numModelRows = this.model.getNumRows();
		if (rowOffset + numRows > numModelRows)
			rowOffset = numModelRows - numRows - 1
		var numFrozenColumns = this.options.numFrozenColumns;
		var frozenColRowIterators = this.model.getRowIterators(rowOffset, numRows, 0);
		var scrollableColRowIterators = frozenColRowIterators;
		if (this.numVisibleCols > numFrozenColumns){
			scrollableColRowIterators = this.model.getRowIterators(rowOffset, numRows, numFrozenColumns+this.colOffset);
		}
		for (var i=0; i < numRows; i++) {
			this.populateRow(this.table.rows[i], frozenColRowIterators[i], scrollableColRowIterators[i]);
		}
		this.lastRowPos = rowOffset;
	},

	refreshAccessibleContents: function() {
		var numModelRows = this.model.getNumRows();
		var iters = this.model.getRowIterators();
		var numCols = this.table.rows[0].cells.length - 1; // skip check column
		var start = new Date().getTime();
		if (this.refreshRowCounter == undefined) this.refreshRowCounter = 0;
		for (var i = this.refreshRowCounter; i < numModelRows; i++) {
			var htmlRowIndex = i+1; // skip header row
			var htmlRow = this.table.rows[htmlRowIndex];
			// if we are rendering for more than 3 seconds, give Firefox some time to get
			// rid of the "unresponsive script" message.
			if(new Date().getTime() - start > 3000){
				setTimeout(this.refreshAccessibleContents.bind(this), 0 );
				return;
			}
						
 			for (var j=0; j < numCols; j++) {
				var gridCell = iters[i].next();
				var htmlCell = htmlRow.cells[j+1]; // skip check column
				if (htmlCell.controller == undefined){
					new Gradebook.CellController(htmlCell, this.grid, htmlRowIndex, j+1, true );
				}
				htmlCell.controller.renderHTML(gridCell);
				// set check box column based on isRowChecked flag for first grid cell
				if (j == 0){
					var htmlCell = htmlRow.cells[0];
					if (htmlCell.controller == undefined){
						new Gradebook.CellController(htmlCell, this.grid, htmlRowIndex, j, true );
					}
					var checkInput = $(htmlCell).down('input');
					checkInput.checked = gridCell.isRowChecked();
				}
			}
		  	var rowTitle = gradebook2Messages['selectUserMsg'];
		    this.grid.getAbbrColIndexs().each(function(idx) {
	          var cell = htmlRow.cells[ idx + 1 ]; // skip checkbox cell
	          rowTitle += " " + cell.controller.getGridCell().getValue();
		    });
		    htmlRow.cells[ 0 ].controller.checkInput.title = rowTitle;
			
			this.refreshRowCounter++;
		}
		this.refreshRowCounter = null;
	},

  refreshContentsH: function() {
	  // refresh data cells
	  this.refreshContents(this.lastRowPos);
	  // refresh the header cells
	  var numFrozenColumns = this.options.numFrozenColumns;
	  var hdrCells = null;
	  var hdr = $(this.table.id+'_header');
	  if (hdr) 
		  hdrCells = hdr.rows[0].cells;
	  else
		  hdrCells = this.table.rows[0].cells;
	  if ( !hdrCells ) return;
	  var frozenColIterator = this.model.getColDefIterator(0);
	  var scrollableColIterator = null;
	  if (this.numVisibleCols > numFrozenColumns){
		  scrollableColIterator = this.model.getColDefIterator(numFrozenColumns+this.colOffset);
	  }
	  for (var i=0; i < this.numVisibleCols; i++) {
		  var iterator = (i < numFrozenColumns)?frozenColIterator:scrollableColIterator;
		  var htmlCell = hdrCells[i+1]; // skip check column
		  var colDef = iterator.next();
		  if (htmlCell.controller == undefined){
			  new Gradebook.CellController(htmlCell, this.grid, 0, i+1, true);
		  }
		  htmlCell.controller.renderHeaderCellHTML( colDef );  
	  }
	  // add the check all listener if not present
	  if ( !hdrCells[0].controller ) new Gradebook.CellController(hdrCells[0], this.grid, 0, 0, true);
	  this.grid.updateSortImage();
   },

   visibleHeight: function() {
      return parseInt(GradebookUtil.getElementsComputedStyle(this.div, 'height'));
   },

	toViewIndex: function(modelSortIndex){
		var numFrozenColumns = this.options.numFrozenColumns;
		if (modelSortIndex < numFrozenColumns){
			return modelSortIndex;
		}
		var vi = (modelSortIndex - this.colOffset);
		if (numFrozenColumns <= vi && vi < this.numVisibleCols) 
			return vi;
		else
			return -1;
	},

	toModelIndex: function(viewSortIndex) {
		if (viewSortIndex == -1) 
			return -1;

		var numFrozenColumns = this.options.numFrozenColumns;
		var mi = (viewSortIndex < numFrozenColumns) ? viewSortIndex : (this.colOffset + viewSortIndex);
		return mi;
	},

   // scrolling management

   initScrollers: function() {
      this.createVScrollBar();
      this.createHScrollBar();
      this.lastVScrollPos = 0;
      if ( this.scrollerDivH != null ) 
	      this.lastHScrollPos = this.scrollerDivH.scrollLeft;
      else 
	      this.lastHScrollPos = 0;
      this.startScrollLeft = this.lastHScrollPos;
   },

   createVScrollBar: function() {
   	// see comments on createHScroolBar()
    if (this.table.rows.length >= this.model.getNumRows()) return;
    var visibleHeight = this.visibleHeight();
    // create the outer div...
    this.scrollerDiv  = document.createElement("div");
    var scrollerStyle = this.scrollerDiv.style;
    scrollerStyle.borderRight = this.options.scrollerBorderRight;
    scrollerStyle.position    = "absolute";
    var tableWidth = this.isIE? this.table.offsetWidth-2+"px" : this.isNS7? this.table.offsetWidth-15+"px" : this.table.offsetWidth-3+"px";
	  if ( document.documentElement.dir == 'rtl' ) 
	    scrollerStyle.right        = tableWidth;
    else 
      scrollerStyle.left        = tableWidth;
    scrollerStyle.top        = "0px";
    scrollerStyle.width       = this.isNS7 ? "30px" : "19px";
    scrollerStyle.height      = visibleHeight + "px";
    scrollerStyle.overflow    = "auto";

    // create the inner div...
    this.heightDiv = document.createElement("div");
    this.heightDiv.style.width  = "1px";
	
	  this.rowHeight = this.table.rows[this.table.rows.length-1].cells[1].offsetHeight;
	  var divHeight = this.rowHeight * this.model.getNumRows();
  	divHeight += ( this.numVisibleRows + 1 ); // looks like we are missing one pix per row, if not added we cannot scroll to the last item
	
    this.heightDiv.style.height = parseInt(divHeight) + "px" ;
    this.scrollerDiv.appendChild(this.heightDiv);
	  Event.observe(this.scrollerDiv,'scroll',this.handleVScroll.bindAsEventListener(this));
	  GradebookUtil.debug('createVScrollBar - this.rowHeight = '+this.rowHeight);
	  GradebookUtil.debug('createVScrollBar - visibleHeight = '+visibleHeight);
	  GradebookUtil.debug('createVScrollBar - numVisibleRows = '+this.numVisibleRows);
	  GradebookUtil.debug('createVScrollBar - this.model.getNumRows() = '+this.model.getNumRows());
	  GradebookUtil.debug('createVScrollBar - this.heightDiv.style.height = '+this.heightDiv.style.height);


  	this.table.parentNode.parentNode.insertBefore( this.scrollerDiv, this.table.parentNode.nextSibling );
    var eventName = this.isIE ? "mousewheel" : "DOMMouseScroll";
	  Event.observe(this.table, eventName, 
	                function(evt) {
	                   if (evt.wheelDelta>=0 || evt.detail < 0) //wheel-up
	                      this.scrollerDiv.scrollTop -= (2*this.rowHeight);
	                   else
	                      this.scrollerDiv.scrollTop += (2*this.rowHeight);
	                   this.handleVScroll();
	                }.bindAsEventListener(this), 
	                false);
     },

   createHScrollBar: function() {
   	 // logic here is to create an div the same width that the non frozen columns
   	 // then put inside it an invisible inner div that would be the width of the non
   	 // frozen if they were all visible; by setting the parent with overflow: auto 
   	 // scroll bars will appear, and the scrolling events are captured to decide what
   	 // portion of the table should be displayed.
	   if (!this.headerTable.rows[0] || this.headerTable.rows[0].cells.length > this.model.getNumColDefs()) return;
	   var totalColumnCount = this.model.getNumColDefs();
	   var visibleColumnCount = this.numVisibleCols;
	   var numFrozenColumns = this.options.numFrozenColumns;
	   this.maxColOffset = totalColumnCount - (visibleColumnCount - numFrozenColumns) - 1;

	   var visibleHeight = this.isIE ? this.table.offsetHeight - 23 : this.isNS7 ? this.table.offsetHeight - 16 : this.table.offsetHeight - 3;
	   var checkColumnWidth = this.headerTable.rows[0].cells[0].offsetWidth;
     // set avg col width to be based on actual cell width (not including padding, etc.)
     // this will allow scrolling to be more accurate
     this.avgColWidth = this.headerTable.rows[0].cells[1].offsetWidth;
	   var frozenWidth = (numFrozenColumns * this.avgColWidth) + checkColumnWidth;
	   visibleWidth = ( visibleColumnCount - numFrozenColumns ) * this.avgColWidth;

	   // create the outer div...
	   this.scrollerDivH  = document.createElement("div");
	   var scrollerStyle = this.scrollerDivH.style;
	   scrollerStyle.position    = "absolute";
	   if ( document.documentElement.dir == 'rtl' ) 
		   scrollerStyle.right        =frozenWidth + "px";
	   else 
		   scrollerStyle.left        = frozenWidth + "px";

	   scrollerStyle.top        = visibleHeight + "px";
	   scrollerStyle.height       = this.isIE ? "40px" : this.isNS7 ? "30px" : "19px";
     scrollerStyle.width      = visibleWidth + "px";
     scrollerStyle.overflow    = "auto";

     // create the inner div...
     this.widthDiv = document.createElement("div");
     this.widthDiv.style.height  = "1px";
     this.widthDiv.style.direction = 'ltr';
     this.widthDiv.style.width = ( this.avgColWidth * ( totalColumnCount-numFrozenColumns ) ) + "px";
     this.scrollerDivH.appendChild(this.widthDiv);
     Event.observe(this.scrollerDivH,'scroll',this.handleHScroll.bindAsEventListener(this));

     if (this.scrollerDiv){
	     	this.table.parentNode.parentNode.insertBefore( this.scrollerDivH, this.scrollerDiv.nextSibling );
	   } else {
		    this.table.parentNode.parentNode.insertBefore( this.scrollerDivH, this.table.parentNode.nextSibling );
	   }
   },

   rowToPixel: function(rowOffset) {
      return (rowOffset / this.model.getNumRows()) * this.heightDiv.offsetHeight
   },
   
   moveScroll: function(rowOffset) {
	if (this.scrollerDiv){
	      this.scrollerDiv.scrollTop = this.rowToPixel(rowOffset);
	}
   },

	/* When scrolling, IE sends multiple onscroll events for a single scroll action by the user.
		To get around this, we set a timer and wait until the dust settles before doing the scroll
		Here is info on the work around: http://support.microsoft.com/kb/238004
	*/
	// scroll numRows, can be negative. returns false if scroll request is out of range
  scrollRows: function(numRows) {
	if (!this.scrollerDiv) return false;
	if ((numRows < 0 && this.scrollerDiv.scrollTop == 0) || 
		(numRows > 0 && this.lastRowPos == (this.model.getNumRows() - this.numVisibleRows))) {
		return false;
	}

	this.ignoreOnVscroll = true;
	this.scrollerDiv.scrollTop += (numRows * this.rowHeight);
      setTimeout(this.doVScroll.bind(this), 200 );
   },

   handleVScroll: function(evt) {
	if (this.ignoreOnVscroll) return;
	this.ignoreOnVscroll = true;
      setTimeout(this.doVScroll.bind(this), 200 );
   },

   doVScroll: function() {
	Gradebook.CellController.prototype.onGridScroll();
	var incomingscrollTop = this.scrollerDiv.scrollTop;
	var scrollDiff = this.lastVScrollPos-this.scrollerDiv.scrollTop;
	if (scrollDiff != 0.00) {
		var r = this.scrollerDiv.scrollTop % this.rowHeight;
		if (r != 0) {
			if (scrollDiff < 0 ) {
				this.scrollerDiv.scrollTop += (this.rowHeight-r);
			} else {
				this.scrollerDiv.scrollTop -= r;
			}
		}
		var contentOffset = parseInt(this.scrollerDiv.scrollTop / parseInt(this.rowHeight));
		GradebookUtil.debug('doVScroll - incomingscrollTop = '+incomingscrollTop+
				' r = '+r+
				' new scrollTop = '+this.scrollerDiv.scrollTop+
				' lastVScrollPos = '+this.lastVScrollPos+
				' contentOffset = '+contentOffset);
		this.refreshContents(contentOffset);
		this.lastVScrollPos = this.scrollerDiv.scrollTop;
	}
	this.ignoreOnVscroll = false;
   },

   handleHScroll: function(evt) {
	if (this.ignoreOnHscroll) return;
	this.ignoreOnHscroll = true;
      setTimeout(this.doHScroll.bind(this), 200 );
   },
	
	// scroll numCols, can be negative. returns false if scroll request is out of
  // range
  scrollCols : function(numCols)
  {
    if (!this.scrollerDivH)
      return false;
    var totalColumnCount = this.model.getNumColDefs();

    if ((numCols < 0 && this.scrollerDivH.scrollLeft == 0)
        || (numCols > 0 && this.colOffset == (this.model.getNumColDefs() - this.numVisibleCols)))
    {
      return false;
    }
    this.ignoreOnHscroll = true;
    /*
     * so here we need to translate delta to actual scroll value. The delta is screen orientation agnostic (we need to move to that col in the model) 
     * we need to translate the move in a pixel move to the left: a move to the left in l2r means we move to the next col,
     * while in r2l it means we move to previous col, thus the inversion of orientation if r2l.
     */
    this.scrollerDivH.scrollLeft += (numCols * this.avgColWidth * (page.util.isRTL()?-1:1) );
    setTimeout(this.doHScroll.bind(this), 200);
    return true;
  },

  doHScroll: function() {
	  Gradebook.CellController.prototype.onGridScroll();
    var scrollDiff = this.lastHScrollPos - this.scrollerDivH.scrollLeft;
	  if (scrollDiff != 0.00) {
	    // To align the column scroll - we move by column increment
		  var r = this.scrollerDivH.scrollLeft % this.avgColWidth;
		  if (r != 0) {
			  if (scrollDiff < 0 ) {
				  this.scrollerDivH.scrollLeft += (this.avgColWidth-r);
			  } else {
				  this.scrollerDivH.scrollLeft -= r;
			  }
		  }
		  if ( document.documentElement.dir == 'rtl' )  {
			  // Subtract the max scroll left with the current one and divide with the avgColWidth 
			  this.colOffset = parseInt( (this.startScrollLeft - this.scrollerDivH.scrollLeft) / this.avgColWidth);
		  } else {
			  this.colOffset = parseInt(this.scrollerDivH.scrollLeft / this.avgColWidth);
		  }
		  this.colOffset = Math.min(this.colOffset,this.maxColOffset);
		  this.refreshContentsH();
		  this.lastHScrollPos = this.scrollerDivH.scrollLeft;
	  }
	this.ignoreOnHscroll = false;
 }

};

	//*********************************************************************
	//************ Gradebook.CellController *******************************
	//*********************************************************************

Gradebook.CellController = Class.create();

Gradebook.CellController.prototype = {

	/* Controls all user interaction with an HTM table cell and its corresponding grid model cell including:
	
		cell-type specific context menus
		sorting by clicking on header cell
		selecting a table cell
		editing of cell value, which includes:
			going into edit mode - showing an input text values to be entered
			validating input as typed
			listening for certain keys to submit or cancel editing
			submitting changes to server and showing "Saving indicator"
		check box in first column for selecting students
		rendering of cell value and state indicators
		grade comment & column into popups

	*/
	initialize: function(htmlCell, grid, row, column, isHeaderTable ) {
	
		this.htmlCell = $(htmlCell);
		this.htmlCell.id = row+','+column;
		this.htmlCell.controller = this;
		this.grid = grid;
		this.row = row;
		this.col = column;
		this.isHeaderTable = isHeaderTable;
		Gradebook.CellController.tableId = this.grid.table.id;
    	var accessibleMode = this.grid.options.accessibleMode;
		this.isTopLeft = (this.row == 0 && this.col == 0) && isHeaderTable;
		if (accessibleMode)
		{
		  this._accessibleInit();
		}
		else
		{
		  this._nonAccessibleInit();
		}
	},

	_nonAccessibleInit: function( ) {
		// get elements in cell
		this.viewDiv = GradebookUtil.getChildElementByClassName(this.htmlCell, 'div', 'gbView');
		this.editDiv = GradebookUtil.getChildElementByClassName(this.htmlCell, 'div', 'gbEdit');
		this.editInput = GradebookUtil.getChildElementByClassName(this.htmlCell, 'input', 'editInput');
		this.textDiv = GradebookUtil.getChildElementByClassName(this.htmlCell, 'div', 'gbText');
		this.dataDiv = GradebookUtil.getChildElementByClassName(this.htmlCell, 'div', 'gbData');
		this.titleAnchor = GradebookUtil.getChildElementByClassName(this.htmlCell, 'a', 'titleAnchor');
		this.contextMenuAnchor = GradebookUtil.getChildElementByClassName(this.htmlCell, 'a', 'cmimg');
		this.checkInput = GradebookUtil.getChildElementByClassName(this.htmlCell, 'input', 'checkInput');
		if ( this.isTopLeft ) {
      		Event.observe(this.checkInput, 'click', this.toggleSelection.bindAsEventListener(this));
      		GradebookViewSpreadsheet.grid.checkAllCellController = this;
      		return;
    	}
		// add listeners to cell & anchors
  		Event.observe(this.htmlCell,'mouseover',this.onMouseOver.bindAsEventListener(this));
		Event.observe(this.htmlCell,'mouseout',this.onMouseOut.bindAsEventListener(this));

		// add listeners to cell elements
		if (this.contextMenuAnchor) {
		  Event.observe(this.contextMenuAnchor,'click',this.onContextMenuClicked.bindAsEventListener(this));
		}

		if (this.row == 0 && this.col != 0 && this.textDiv && this.dataDiv) {
			this.getGridCell = this.getHeaderGridCell;
			Event.observe(this.textDiv,'click',this.onHeaderClicked.bindAsEventListener(this));
			Event.observe(this.dataDiv,'focus', this.showHeaderInfoInTaskbar.bindAsEventListener(this));
	    	Event.observe(this.dataDiv,'mouseover', this.showHeaderInfoInTaskbar.bindAsEventListener(this));
	    	Event.observe(this.dataDiv,'mouseout', this.onHeaderMouseOut.bindAsEventListener(this));
			this.htmlCell.style.cursor  = 'pointer';
		} else {
			this.getGridCell = this.getGradeGridCell;
			if (this.editInput){
				Event.observe(this.editInput,'keydown',this.onInputKeyDown.bindAsEventListener(this));
				Event.observe(this.editInput,'keyup',this.onInputKeyUp.bindAsEventListener(this));
			}			
      		if (this.checkInput) {
				Event.observe(this.checkInput,'click',this.onCheckBoxClicked.bindAsEventListener(this));
			} else {
				Event.observe(this.htmlCell,'click',this.onClicked.bindAsEventListener(this));
			}
			if ( this.titleAnchor ) Event.observe(this.titleAnchor,'focus',this.onFocus.bindAsEventListener(this) );
		}
	},
		
	_accessibleInit: function( ) {
		// get elements in cell
		this.checkInput = this.htmlCell.down('input');
		this.titleAnchor = this.htmlCell.down('a');
		this.contextMenuAnchor = (this.titleAnchor) ? this.titleAnchor : this.htmlCell;

		if ( this.isTopLeft ) 
		{
      		Event.observe(this.checkInput, 'click', this.toggleSelection.bindAsEventListener(this));
      		GradebookViewSpreadsheet.grid.checkAllCellController = this;
      		return;
    	}

		if (this.row == 0 && this.col != 0)
		{
			this.dataDiv = this.titleAnchor; 
			this.getGridCell = this.getHeaderGridCell;
			Event.observe(this.titleAnchor,'focus', this.showHeaderInfoInTaskbar.bindAsEventListener(this));
	    	Event.observe(this.htmlCell,'mouseover', this.showHeaderInfoInTaskbar.bindAsEventListener(this));
	    	Event.observe(this.htmlCell,'mouseout', this.onHeaderMouseOut.bindAsEventListener(this));
		} 
		else 
		{
			this.getGridCell = this.getGradeGridCell;
		}

  		if (this.checkInput) 
  		{
			Event.observe(this.checkInput,'click',this.onCheckBoxClicked.bindAsEventListener(this));
		}
		else if (this.getGridCell().getContextMenuInfo(this)) 
		{
			Event.observe(this.contextMenuAnchor,'click',this.onContextMenuClicked.bindAsEventListener(this));
		}
		else
		{
			// no link/menu for calculated grades, grade goes right in cell 
			this.titleAnchor.remove();
			this.titleAnchor = this.htmlCell;
		}
	},
		
	isHeaderCell: function()
	{
		return this.isHeaderTable;
	},

	unload: function() {
		this.grid = null;
		this.htmlCell.controller = null;
		this.htmlCell = null;
		this.grid = null;
		this.viewDiv = null;
		this.editDiv = null;
		this.editInput = null;
		this.textDiv = null;
		this.dataDiv = null;
		this.titleAnchor = null;
		this.contextMenuAnchor = null;
		this.checkInput = null;
		this.getGridCell = null;
		this.editGridcell = null;
	},

	//************ checkbox logic *******************************

	onCheckBoxClicked: function(evt) {
		var gridcell = this.getGridCell();
		gridcell.setRowChecked(this.checkInput.checked);
		var userId = gridcell.userId;
		if(this.checkInput.checked){
			if (evt.shiftKey && Gradebook.CellController.prototype.lastCheckedUserId){
				this.grid.model.checkedRangeOfStudents(gridcell.userId,Gradebook.CellController.prototype.lastCheckedUserId);
			}
			Gradebook.CellController.prototype.lastCheckedUserId = gridcell.userId;
		} else {
			Gradebook.CellController.prototype.lastCheckedUserId = null;
		}
		
		this.updateNumSelectedIndicator();
	},

	toggleSelection: function() {
		if( this.checkInput.checked ) {
			this.onSelectAllStudents();
		} else {
			this.onSelectNoStudents();
		}
	},

	onSelectAllStudents: function(evt) {
		this.grid.model.checkedAllStudents();
		this.updateNumSelectedIndicator();
	},

	onSelectNoStudents: function(evt) {
		this.grid.model.checkedNoStudents();
		this.updateNumSelectedIndicator();
	},
	
	onSelectInvertStudents: function(evt) {
		this.grid.model.invertCheckedStudents();
		this.updateNumSelectedIndicator();
	},
	
	onSortCheckedStudents: function(evt) {
		// always show checked students at top
		this.grid.sortColumn(this,'DESC');
	},
	
	updateNumSelectedIndicator: function() {
		var ids = this.grid.model.getCheckedStudentIds();
		$("rowindicator").update( ids.length );
	},
	
	
	//************ sort logic *******************************

	onHeaderClicked: function(evt) {
		this.grid.sortColumn(this);
	},

	setSortImage: function(dir) {
		this.htmlCell.removeClassName('sortedUp');
		this.htmlCell.removeClassName('sortedDown');
		if ( dir == 'ASC' ){
			this.htmlCell.addClassName('sortedUp');
		} else if ( dir == 'DESC' ){
			this.htmlCell.addClassName('sortedDown');
		}
	},

	onSortAscending: function(dir) {
		this.grid.sortColumn(this,'ASC');
		this.contextMenuDiv.hide();
	},
	
	onSortDescending: function(dir) {
		this.grid.sortColumn(this,'DESC');
		this.contextMenuDiv.hide();
	},
	
	//************ select cell logic *******************************

  onFocus: function( evt )
  {
	  document.ignoreOnClick = true; //IE7 issue where focus is followed by on click on the document - AS-123689
	  window.setTimeout( "document.ignoreOnClick = false", 2000 );
	  this.onClicked( evt );
  },
	
	onClicked: function(evt) {
    var eventTarget = evt.target ? evt.target : evt.srcElement;
		Gradebook.CellController.prototype.lastEventTarget = eventTarget;
		this.selectCell( eventTarget );
		Event.stop( evt );
  },
   	
	isSelected: function() {
		return (Gradebook.CellController.currentSelectedCell == this.htmlCell);
	},
	
	selectCell: function( optionalEventTarget ) {
	  Gradebook.CellController.prototype.tableHasFocus = true;
		if ( this.isSelected() || this.checkInput )	return;
		this.closePopups();
		this.unselectCurrentCell( true /*do not clear status bar */ );
		GradebookUtil.debug('selectCell row = '+this.row+' col = '+this.col);
		var gridCell = this.getGridCell();
		Gradebook.CellController.currentSelectedCell = this.htmlCell;
		var hascm = this.hasContextMenu();
		Element.addClassName(this.htmlCell, hascm?"cellClick":"cellClickNoCM")
		Element.addClassName(this.htmlCell.parentNode, "focusRowHigh");
		var headerTable = $(Gradebook.CellController.tableId + '_header');
		if (headerTable){
			Element.addClassName(headerTable.rows[0].cells[this.col],"focusHeader");
		}	
		if (!this.isEditing && this.titleAnchor){
		  // no need to put focus on the anchor if it is already the active element
			if ( !optionalEventTarget || ( optionalEventTarget != this.titleAnchor ) ) this.titleAnchor.focus();
		} else if ( this.grid.options.accessibleMode ) {
		    this.htmlCell.focus();
		}
		this.setTaskbarInfo(gridCell);
		if (!this.grid.options.accessibleMode) this.startEdit();
	},

	showHeaderInfoInTaskbar: function() {
	  var colDef = this.getGridCell();
	  var type = colDef.getType(); 
	  if ( type == "student" ) {
      GradebookViewSpreadsheet.setMsgInTaskBar( colDef.getName() );
	  } else {
	    if ( type == "grade" )
	      points = NumberFormatter.getDisplayFloat( colDef.points );
	    else 
	      points = GradebookUtil.getMessage( 'variesPerStudentMsg' );
      GradebookViewSpreadsheet.setHeaderInfoInTaskBar( colDef.getName(), 
          GradebookUtil.getMessage( colDef.getType() + 'Msg' ), 
          points );
	  }
	},
	
	setTaskbarInfo: function(gridCell) {
	    if (!gridCell) { 
	   		GradebookViewSpreadsheet.clearTaskBar();
	    } else if (gridCell.isGrade()) {
			var colDef = gridCell.colDef;
		    var gradeType = '&nbsp;'
		    var pointsPossible = '&nbsp;'
		    var primaryDisplay = '&nbsp;'
		    var visibileToStudents = '&nbsp;'
			try {
				if ( gridCell.canEdit() )
				{ 
					gradeType = GradebookUtil.getMessage((gridCell.isOverride())?'overrideGradeMsg':'gradeMsg');
				}
				else
				{ 
					gradeType = GradebookUtil.getMessage( colDef.getType() + 'Msg' );
				}
				primaryDisplay = colDef.primarySchema.name;
				pointsPossible = NumberFormatter.getDisplayFloat( gridCell.getPointsPossible() );
				visibileToStudents = GradebookUtil.getMessage((colDef.vis)?'isMsg':'isNotMsg');
			} catch ( ignore ) { };
		    GradebookViewSpreadsheet.setTaskBar(gradeType,pointsPossible,primaryDisplay,visibileToStudents);
		} else {
		    GradebookViewSpreadsheet.setTaskBar();
		}
	},

	unselectCell: function( doNotClearStatusBar ) {
		GradebookUtil.debug('unselectCell row = '+this.row+' col = '+this.col);
		Element.removeClassName(this.htmlCell, "cellClick");
		Element.removeClassName(this.htmlCell, "cellClickNoCM");
		Element.removeClassName(this.htmlCell.parentNode, "focusRowHigh");
		var headerTable = $(Gradebook.CellController.tableId + '_header');
		if (headerTable){
			Element.removeClassName(headerTable.rows[0].cells[this.htmlCell.cellIndex],"focusHeader");
		}	
		if ( !doNotClearStatusBar ) this.setTaskbarInfo();
	},

	unselectCurrentCell: function( doNotClearStatusBar ) {
		var cell = Gradebook.CellController.currentSelectedCell;
		if (cell) 
		{
   	  var commit = false;
			var cellController = cell.controller;
			if ( cellController) 
			{
			  if ( cellController.hasUncommittedChanges())
			  {
				    var validationError = cellController.editGridcell.validate(cellController.editInput.value);
				    if (!validationError)
				    {
					    commit = confirm(GradebookUtil.getMessage('uncommitedchangeErrorMsg'));
				    } 
				    else 
				    {
					    alert(GradebookUtil.getMessage('uncommitedchangeNotSavedErrorMsg'));
				    }
			  }
			  cellController.stopEdit(commit, true /* no focus on the cell we are exiting */);
			  cellController.unselectCell( doNotClearStatusBar );
	    }
		  Gradebook.CellController.currentSelectedCell = null;
		}
	},

	//************ edit grade logic *******************************
	
	startEdit: function(){
		try {
			this.editGridcell = this.getGridCell();
			if (!this.editGridcell.canEdit() || !this.isSelected() || !this.editInput) return;
			GradebookUtil.debug('startEdit row = '+this.row+' col = '+this.col);
			this.isEditing = true;
			this.editInput.value = this.editGridcell.getEditValue();
			this.viewDiv.style.display = "none";
			this.editDiv.style.display = "block";
			this.editInput.focus();
			this.editInput.select();
		} catch ( ignore ) { }
	},

	onInputKeyDown: function(evt){
		GradebookUtil.debug('onInputKeyDown row = '+this.row+' col = '+this.col+' keyCode = '+evt.keyCode);
		switch (evt.keyCode) {
			case (Event.KEY_TAB):
				this.stopEdit(true, false); //commit, Set Focus back to cell content
				Event.stop( evt );
				break;
		}
	},
	
	onInputKeyUp: function(evt){
		Event.stop( evt );
		GradebookUtil.debug('onInputKeyUp row = '+this.row+' col = '+this.col+' keyCode = '+evt.keyCode);
		switch (evt.keyCode) {
			case (Event.KEY_UP):
			case (Event.KEY_DOWN):
			case (Event.KEY_LEFT):
			case (Event.KEY_RIGHT):
				evt.cancelBubble = false; // allow event to bubble so attempted navigation will occur
				break;
			case (Event.KEY_RETURN):
			   try
	       {
	         Gradebook.noResize = true; /*IEHack*/
				   if (this.hasUncommittedChanges() && this.stopEdit(true)){ //commit
					   // select cell below
					   this.grid.selectRelativeCell(1, 0);
				   }
	       }
			   finally
			   {
			     Gradebook.noResize = false;
			   }
				break;
			case (Event.KEY_ESC):		
				this.stopEdit(false); // don't commit
				break;
			default:
				var validationError = this.editGridcell.validate( this.editInput.value, true ); // match partial
				if (validationError){
					this.showValidationError(validationError);
				} else {
					this.hideValidationError();
				}
		}
	},	

	// returns false if validation error occurs when committing
	stopEdit: function(commit, doNotSetFocus){
		if (!this.isEditing){
			return;
		}
		GradebookUtil.debug('stopEdit row = '+this.row+' col = '+this.col+' commit = '+commit);
		if (commit && this.hasUncommittedChanges()){
			var inputVal = this.editInput.value;
			var validationError = this.editGridcell.validate( inputVal );
			if (validationError){
				this.showValidationError(validationError);
				this.editInput.select();
				this.editInput.focus();
				return false;
			}
			var save = true;
			if (inputVal == '') inputVal = '-';
			// confirm if OK to delete or null grade
			if (inputVal == '-'){
				if (this.editGridcell.v == '-'){
					save = false;
				} else {
					var msg = (this.editGridcell.isOverride()?'confirmRevertMsg':'confirmNullMsg');
					save = confirm(GradebookUtil.getMessage(msg));
				}
			}
			if (save){
				// send update to server
				this.editGridcell.update(inputVal);
				
				// show saving message
				this.addSavingDiv();			
			}
		}
		this.hideValidationError();
		this.isEditing = false;
		this.editGridcell = null;
		this.viewDiv.style.display = "block";
		if (!doNotSetFocus){
			this.titleAnchor.focus();
		}
		this.editDiv.style.display = "none";
		return true;
	},

	hasUncommittedChanges: function(evt){
		return (this.isEditing && this.editInput.value != this.editGridcell.getEditValue());
	},	

	//************ rendering logic *******************************

	renderHTML: function(gridCell) {
		var anchorVal;
		var altVal;
		if (gridCell.savingDiv){
			this.showSavingDiv(gridCell.savingDiv);
           	gridCell.savingDiv.htmlCell = this.htmlCell;
		} else if (gridCell.isExempt()){
			anchorVal = gridImages.exemptGrade;
			altVal = gridCell.getAltValue();
		} else if (gridCell.needsGrading() && !gridCell.isOverride()){
			anchorVal = gridImages.needsGrading;
		} else if (gridCell.attemptInProgress() && !gridCell.isOverride()){
			anchorVal = gridImages.attemptInProgress;
		} else if (gridCell.isComplete()){
			anchorVal = gridCell.getCellValue();
			altVal = GradebookUtil.getMessage('completedMsg');
		} else if ( this.grid.options.accessibleMode && gridCell.isGrade() && !gridCell.isGraded() ) {
		    anchorVal = gridImages.noGrade;
		    altVal = gridCell.getAltValue();
		} else {
			anchorVal = gridCell.getCellValue();
			altVal = gridCell.getAltValue();
		}
		if (anchorVal != undefined){
			if (this.col == 1 && !gridCell.isAvailable()){
				anchorVal = gridImages.studentUnavailable+" "+anchorVal;
			}
			if (gridCell.isModified()){
				anchorVal = gridImages.gradeModified+" "+anchorVal;
			}
			if (anchorVal.blank()){
				anchorVal = '&nbsp;'
			}
			if (this.titleAnchor){
				this.titleAnchor.innerHTML = anchorVal;
				this.titleAnchor.title = altVal;
			}
		}
	},

	renderHeaderCellHTML: function( colDef ) {
		var anchorVal = '';
		var title = colDef.name.unescapeHTML();
		// IE hack so that unicode are properly escaped
    this.dataDiv.innerHTML = title;
    this.dataDiv.title = this.dataDiv.innerHTML;
		if (!colDef.isVisibleToStudents()){
			anchorVal += gridImages.itemNotVisible;
		}
		if (colDef.isPublic()){
			anchorVal += gridImages.externalGrade;
		}
		if (colDef.hasError()){
			anchorVal += gridImages.gradingError;
		}
		anchorVal += title;
		this.dataDiv.innerHTML = anchorVal;
	},
	
   
	addSavingDiv: function() {
		// create a "Saving" message by cloning the existing message
		var savingDiv = $("saveDiv").cloneNode(true);
		document.body.appendChild(savingDiv);
		// associate savingDiv with gridCell so saving message will scroll with gridCell
		var gridCell = this.editGridcell;
		if (!gridCell) gridCell = this.getGridCell();
       	gridCell.savingDiv = savingDiv;
       	savingDiv.gridCell = gridCell;
       	savingDiv.htmlCell = this.htmlCell;
       	
		// store savingDivs in a class-level array to allow hiding all savingDivs before scrolling refresh
		if (!Gradebook.CellController.prototype.savingDivs){
			Gradebook.CellController.prototype.savingDivs = new Array();
		}
		Gradebook.CellController.prototype.savingDivs.push(savingDiv);
		
		// show the savingDiv on top of htmlCell
		this.showSavingDiv(savingDiv);
		
		// show div for a period of time, then hide/remove it if grade change has been committed on server
		setTimeout(function(){
				var gc = savingDiv.gridCell;
				if (gc.isUncommitted){
					savingDiv.timerExpired = true;
				} else {
					savingDiv.style.display = 'none';
					savingDiv.parentNode.removeChild(savingDiv);
					gc.savingDiv = null;
					savingDiv.htmlCell.controller.renderHTML(gc);
				}
			},1000);
	},

	showSavingDiv: function(savingDiv){
    var pos = Element.cumulativeOffset( this.htmlCell );
		savingDiv.style.top = pos.top + "px";
	  savingDiv.style.left = pos.left + "px";
	  if ( !this.htmlCell ) return; 
		savingDiv.style.minWidth = this.htmlCell.offsetWidth + "px";
		savingDiv.style.minHeight = this.htmlCell.offsetHeight + "px";
	  savingDiv.style.display = "block";
	},

	hideAllSavingDivs: function(){
		var savingDivs = Gradebook.CellController.prototype.savingDivs;
		if (!savingDivs) return;
		for (var i = savingDivs.length-1; i >= 0; i--){
			if (!savingDivs[i].parentNode){
				// remove div if no longer in document
				savingDivs.splice(i,1); 
			} else {
				savingDivs[i].style.display = 'none';
			}
		}
	},
	
	removeAllSavingDivs: function(){
		var savingDivs = Gradebook.CellController.prototype.savingDivs;
		if (!savingDivs) return;
		for (var i = savingDivs.length-1; i >= 0; i--){
			var savingDiv = savingDivs[i];
	       	if (savingDiv.gridCell)
	       	{
	       		savingDiv.gridCell.savingDiv = null;
	       		savingDiv.gridCell = null;
	       		savingDiv.htmlCell = null;
	       	}
			if (!savingDivs[i].parentNode){
				// remove div if no longer in document
				savingDivs.splice(i,1); 
			} else {
				savingDivs[i].style.display = 'none';
			}
		}
	},
	
	showValidationError: function(error) {
		var errDiv = $("errorDiv");
		var p = GradebookUtil.getChildElementByClassName(errDiv, 'p', 'errorDiv2');
		p.innerHTML = error;
    errDiv.style.display = "block";
		var pos = GradebookUtil._toAbsolute(this.htmlCell, false, errDiv.offsetParent );
		errDiv.style.top = pos.y + this.htmlCell.offsetHeight + "px";
		errDiv.style.left = pos.x -1 + "px";
		Element.addClassName(this.htmlCell, "cellError");
	},
	
	hideValidationError: function() {
		var errDiv = $("errorDiv");
		errDiv.style.display = "none";
		Element.removeClassName(this.htmlCell, "cellError");
	},
	
	hasContextMenu: function() {
		if ( this.isTopLeft ){
			return true;
		} else {
			return (this.getGridCell().getContextMenuInfo(this) != null);
		}
	},
	
	onMouseOver: function(evt) {
		if (!this.htmlCell || this.htmlCell.className == "cellClick") return;
		var hascm = this.hasContextMenu();
		Element.addClassName(this.htmlCell, hascm?"cellhigh":"cellhighNoCM")
		var rowElement = this.htmlCell.parentNode;
		if (rowElement.className != "focusRowHigh"){
			Element.addClassName(rowElement, "rowhigh");
		}
	},   
	
	onMouseOut: function(evt) {
		if (!this.htmlCell || this.htmlCell.className == "cellClick") return;
		Element.removeClassName(this.htmlCell, "cellhigh")
		Element.removeClassName(this.htmlCell, "cellhighNoCM")
		var rowElement = this.htmlCell.parentNode;
		if (rowElement.className != "focusRowHigh"){
			Element.removeClassName(rowElement, "rowhigh");
		}
	},
	
  onHeaderMouseOut: function( evt ) {
	  if ( Gradebook.CellController.currentSelectedCell ) {
	    var selectedCell = Gradebook.CellController.currentSelectedCell.controller;
	    selectedCell.setTaskbarInfo( selectedCell.getGridCell() );
	  } else {
	    GradebookViewSpreadsheet.clearTaskBar();
	  }
  },


	//************ context menu logic *******************************

	onContextMenuClicked: function(evt) {
		GradebookUtil.debug('onContextMenuClicked');
		Gradebook.CellController.prototype.closePopups(evt);
		var gridCell = this.getGridCell();
		var menuInfo = gridCell.getContextMenuInfo(this);
		if (menuInfo) {
			this.setContextMenuInfo(menuInfo);
		}
		Event.stop( evt );
	},

	onCloseContextMenu: function(evt) {
		Event.stop( evt );
		if ( GradebookUtil.isIE() ){
		    if ( !this.isHeaderCell() ) 
		    {
				this.selectCell();
			}
		    else
		    {
		    	this.contextMenuAnchor.focus();
		    }
		} else {
			this.contextMenuAnchor.focus();
		}
		this.contextMenuDiv.hide();
        $("shimDiv").hide();
	},
	
	onContextMenuTabPress: function(evt, withShift)
    {	
		var ek = evt.keyCode || evt.which;
    	if ( (ek == Event.KEY_TAB) && ( withShift == evt.shiftKey ) )
    	{
    		this.onCloseContextMenu(evt);
    		Event.stop( evt );
    	}
    },    
	
	onContextMenuKeyDown: function(evt)
    {	
		var key = evt.keyCode || evt.which;
	    if ( key == Event.KEY_UP )
	    {  
	      var elem = Event.element ( evt );
	      var index = this.menuItemLinks.indexOf( elem );
	      if ( index > 0 )
	      {
	        this.menuItemLinks[index - 1].focus();
	      } 
	      Event.stop( evt );
	    }
	    else if ( key == Event.KEY_DOWN )
	    {
	      var elem = Event.element ( evt );
	      var index = this.menuItemLinks.indexOf( elem );
	      if ( index < ( this.menuItemLinks.length - 1 ) )
	      {
	        this.menuItemLinks[index + 1].focus();
	      }   
	      Event.stop( evt );
	    }
	    else if ( key == Event.KEY_ESC )
	    {
	      this.onCloseContextMenu(evt);
	      Event.stop( evt );
	    }
    },    
	
	setContextMenuInfo: function(menuInfo) {
		this.contextMenuId = menuInfo.id;
		var menuDiv = $(this.contextMenuId);
		this.contextMenuDiv = menuDiv;
		var firstItem = null;
		var lastItem = null;
		if (menuDiv.onkeydownHandler){
			Event.stopObserving(menuDiv, 'keydown', menuDiv.onkeydownHandler);
		}
	    menuDiv.onkeydownHandler = this.onContextMenuKeyDown.bindAsEventListener( this );
	    Event.observe( menuDiv, "keydown", menuDiv.onkeydownHandler );
		
		this.menuItemLinks = new Array();
		var onCloseHandler = this.onCloseContextMenu.bindAsEventListener(this);
		menuInfo.items.push({id: 'close_'+menuInfo.id, visible:true,
					onclick: onCloseHandler});
		menuInfo.items.each(function(mi) {
			var itemLink = $(mi.id); 
			itemLink.parentNode.style.display = (mi.visible)?"block":"none";
			if (mi.visible)
			{
				this.menuItemLinks.push( itemLink );
			}
			// remove previous click handler, if any
			if (itemLink.onclickHandler){
				Event.stopObserving(mi.id, 'click', itemLink.onclickHandler);
				itemLink.onclickHandler = null;
			}
			if (mi.onclick && mi.visible){
				// add click handler for menu item and save for later removal
				itemLink.onclickHandler = function( evt ) {
				    onCloseHandler( evt );
				    mi.onclick( evt );
					if ( mi.receipt )
					{
				    	GradebookUtil.showInlineReceipt( gradebook2Messages[mi.receipt] );
				    }
				}
				Event.observe(mi.id, 'click', itemLink.onclickHandler);
			}
			if (!firstItem) firstItem = itemLink;
			lastItem = itemLink;
		}.bind(this));
		
		// Handle tabbing out of the menu
		if ( lastItem.previousTABListener ) Event.stopObserving( lastItem.id, 'keydown', lastItem.previousTABListener );
		var tabListener = this.onContextMenuTabPress.bindAsEventListener(this, false);
		lastItem.previousTABListener = tabListener;
		Event.observe( lastItem.id, 'keydown', tabListener );
		if ( firstItem.previousTABListener ) Event.stopObserving( firstItem.id, 'keydown', lastItem.previousTABListener );
		var shiftTabListener = this.onContextMenuTabPress.bindAsEventListener(this, true);
		firstItem.previousTABListener = shiftTabListener;
		Event.observe( firstItem.id, 'keydown', shiftTabListener );
		
		// position and show menu
		var offset = Position.cumulativeOffset( this.contextMenuAnchor);
		if ( this.grid.options.accessibleMode )
		{
		  var tableContainer = $( 'table1_accessible_container' );
		  offset[0] -= tableContainer.scrollLeft;
		  offset[1] -= tableContainer.scrollTop;
		}
		menuDiv.setStyle({display: "block"});
		var width = menuDiv.getWidth();
		var bodyWidth = $(document.body).getWidth();
		if ( page.util.isRTL() )
		{
		  offset[0] = offset[0] + this.contextMenuAnchor.getWidth() - width;
		  if ( offset[0] < 0 ) offset[0]= 0;
		}
		if ( offset[0] + width > bodyWidth )
		{
		  offset[0] = offset[0] - width + this.contextMenuAnchor.getWidth();
		}
		var ypos = offset[1] + this.contextMenuAnchor.getHeight();
		menuDiv.setStyle({ left: offset[0] + "px", top: ypos + "px"});
		if ( GradebookUtil.isFFonMac() ) GradebookUtil.shimDiv( menuDiv );
		(function() { firstItem.focus(); }).defer();         
	},
    
	
	//************ comments logic *******************************

	addGradeComment: function(evt,colDef) {
  	Event.stop( evt ); // swallow event
  	this.closePopups();
    // here rather than on close to fix a UI glitch 1st time the div is shown
    GradebookViewSpreadsheet.instructorCommentsResize._reset();
    GradebookViewSpreadsheet.studentCommentsResize._reset();
    var pos = GradebookUtil._toAbsolute(this.htmlCell);
		var submitCommentsButton = $("submitCommentsButton");
		if (submitCommentsButton.onclickHandler){
			Event.stopObserving(submitCommentsButton, 'click', submitCommentsButton.onclickHandler);
		}
		submitCommentsButton.onclickHandler = this.onSubmitComments.bindAsEventListener(this);
		Event.observe(submitCommentsButton,'click',submitCommentsButton.onclickHandler);
		var commentsDiv = $("commentsDiv");
		if (commentsDiv.onclickHandler){
			Event.stopObserving(commentsDiv, 'click', commentsDiv.onclickHandler);
		}
		commentsDiv.onclickHandler = this.onClickCommentsDiv.bindAsEventListener(this);
		Event.observe(commentsDiv,'click',commentsDiv.onclickHandler);
		var ie = GradebookUtil.isIE();
    var rightedge = ie ? document.body.clientWidth: window.innerWidth;
    var bottomedge = ie ? document.body.clientHeight: window.innerHeight;
    var offright=false;
		var offbottom=false;
    if( pos.y+commentsDiv.offsetHeight>bottomedge ) offbottom = true;
    if( pos.y-commentsDiv.offsetHeight<0 )offbottom = false;
    if( pos.x+commentsDiv.offsetWidth>rightedge-20 ) offright = true;
    if( offbottom )
    {
      $("commentArrowUp").style.display="none";
      $("commentArrowDown").style.display="block";
      $("commentArrowDown").className="bubArrowBot";
	    pos.y=pos.y-commentsDiv.offsetHeight;
    }
    else
    {
      $("commentArrowUp").style.display="block";
      $("commentArrowDown").style.display="none";
      $("commentArrowUp").className="bubArrowTop";
		}
    if ( offright )
    {
      $("commentArrowDown").className="bubArrowBot2";
      $("commentArrowUp").className="bubArrowTop2";
      pos.x=pos.x-200;
    }
		commentsDiv.style.top = ( pos.y - this.htmlCell.offsetHeight )+"px";
		commentsDiv.style.left = pos.x+"px";
    commentsDiv.style.display="block";
		if ( GradebookUtil.isFFonMac() ) GradebookUtil.shimDiv( commentsDiv );
	},

	onSubmitComments: function() {
		this.getGridCell().setComments($("studentComments").value, $("instructorComments").value);
		this.addSavingDiv();
		this.closeComments();
	},

	onClickCommentsDiv: function(evt) {
      	var eventTarget = evt.target ? evt.target : evt.srcElement;
		Gradebook.CellController.prototype.lastCommentsEventTarget = eventTarget;
	},

	testCommentsOpen: function(evt) {
		if (!evt) return;
		var ctrl = Gradebook.CellController.prototype;
		var eventTarget = evt.target ? evt.target : evt.srcElement;
		// if editing comments prompt user to save if click outside comments div
		if (parseInt($("commentsDiv").style.display != 'none') && 
			ctrl.lastCommentsEventTarget != eventTarget) {
			if (confirm(GradebookUtil.getMessage('uncommitedCommentChangeErrorMsg'))){
				$("submitCommentsButton").onclick();
			} else {
				ctrl.closeComments();
			}
		}
	},

	closeComments: function() {
		$("commentsDiv").style.display="none";
		var submitCommentsButton = $("submitCommentsButton");
		if (submitCommentsButton.onclickHandler){
			Event.stopObserving(submitCommentsButton, 'click', submitCommentsButton.onclickHandler);
			submitCommentsButton.onclickHandler = null;
		}
		var commentsDiv = $("commentsDiv");
		if (commentsDiv.onclickHandler){
			Event.stopObserving(commentsDiv, 'click', commentsDiv.onclickHandler);
			commentsDiv.onclickHandler = null;
		}
		$("shimDiv").style.display="none";
	},

	//************ miscellaneous *******************************

	closePopups: function(evt) {
		$("gradeHeaderCM").style.display="none";
		$("studentInfoHeaderCM").style.display="none";
  	$("gradeCM").style.display="none";
		$("studentInfoCM").style.display="none";
		$("infodiv").style.display="none";
		$("icondiv_up").style.display="none";
		$("icondiv_down").style.display="none";
		$("shadow").style.display = "none";
		if ( Gradebook.doNotCloseAttemptsForm )
		{
			Gradebook.doNotCloseAttemptsForm = false;
		}
		else
		{
		  $("clearAttemptsFlyOut").style.display = "none";
		}
		Gradebook.CellController.prototype.testCommentsOpen(evt);
		$("shimDiv").style.display="none";
	},
	
	getGradeGridCell: function(){
		return this.grid.viewPort.getModelGridCell(this.row, this.col);
	},
	
	getHeaderGridCell: function(){
		return this.grid.viewPort.getHeaderGridCell(this.col);
	},
	
	closePopupsAndRestoreFocus: function(evt) {
		var ctrl = Gradebook.CellController.prototype;
		ctrl.closePopups(evt);
		var eventTarget = evt.target ? evt.target : evt.srcElement;
		if (ctrl.lastEventTarget == eventTarget){
			ctrl.tableHasFocus = true;
		}
		else if (ctrl.tableHasFocus) {
			ctrl.unselectCurrentCell();
			ctrl.tableHasFocus = false;
		}
	},

  showClearAttemptsFlyOut: function( event, colDef )
	{
	  var linkElement = $('gh_clearAllAttempts');
	  var offset = Position.cumulativeOffset( linkElement );
	  var formDiv = $('clearAttemptsFlyOut');
	  var width = formDiv.getWidth();
	  var bodyWidth = $(document.body).getWidth();
	  // reposition form if it goes off the screen
	  // align right edge of flyout form with link button
	  if ( offset[0] + width > bodyWidth )
	  {
	    offset[0] = (offset[0] + linkElement.getWidth()) - width;
	  }
	  formDiv.setStyle({
	        left: offset[0] + "px",
	        top: ( offset[1] - Gradebook.Grid.pageHeightOffset ) + "px",
	        display: "block"
	      });
	  $("gradeHeaderCM").hide();
	  // restoring default values
	  if ( Gradebook.clearAttemptsFormDefault )
	  {
	    $('selectOption').value = Gradebook.clearAttemptsFormDefault.defaultSelect;
	    $('dp_bbDateTimePicker_start_date').value = Gradebook.clearAttemptsFormDefault.defaultStartDate;
	    $('dp_bbDateTimePicker_end_date').value = Gradebook.clearAttemptsFormDefault.defaultEndDate;
	    $('bbDateTimePickerstart').value = Gradebook.clearAttemptsFormDefault.defaultStartDateHidden;
	    $('bbDateTimePickerend').value = Gradebook.clearAttemptsFormDefault.defaultEndDateHidden;
	  }
	  $('clearAttemptsOptionSelect').checked = true;
	  $('clearAttemptsFlyOutSubmit').onclick = this.onSubmitClearAttempts.bindAsEventListener( this ) ;  	  
	  Event.stop( event );
	},

	onSubmitClearAttempts: function( event ) {
		Event.stop( event );
		if ( !confirm( gradebook2Messages.clearAttemptConfirmMsg ) ) return false;
		if ( $('clearAttemptsOptionSelect').checked )
		{
			this.getGridCell().clearAttempts( $('selectOption').value );
		}
		else
		{
            var startDate = $('bbDateTimePickerstart').value;
            var endDate = $('bbDateTimePickerend').value;
            this.getGridCell().clearAttemptsByDate(startDate, endDate);
		}
		return false;
	},

	viewColumnInfo: function(evt,colDef) {
        Event.stop( evt ); // swallow event
  	    this.closePopups(evt);
	 	var info = colDef.getInfo();
		info.each(function(ii) {
			$(ii.id).innerHTML = " "+ii.value;
		});
		var pos = GradebookUtil._toAbsolute(this.htmlCell);
		var infoDiv = $('infodiv');
		var ie = GradebookUtil.isIE();
        var rightedge = ie ? document.body.clientWidth: window.innerWidth;
        if(pos.x+infoDiv.offsetWidth>rightedge-20){
		  pos.x=pos.x-infoDiv.offsetWidth+50;
          $("bubbleArrowTop").className="bubArrowTop2";
		}else{
          $("bubbleArrowTop").className="bubArrowTop";
		}
		infoDiv.style.top = ( pos.y - this.htmlCell.offsetHeight ) + "px";
		infoDiv.style.left = pos.x+"px";
		infoDiv.style.display = "block";
		if ( GradebookUtil.isFFonMac() ) GradebookUtil.shimDiv( infoDiv );
	},

	sendEmail: function(type,studentIds){
		var sendEmailFunc = this.grid.options.sendEmailFunc;
		if (sendEmailFunc)
		{
			sendEmailFunc(type,studentIds);
		}
	},

	onGridScroll: function(){
		this.closePopups();
		this.unselectCurrentCell();
	}

};	


var GradebookUtil = {

   isValidFloat: function ( n ) {
   		var n = ''+n;
		var trimmedVal = n.strip();
		var numberFormatDecimalPoint = null;
		if ( window.LOCALE_SETTINGS )
		{
		   numberFormatDecimalPoint = LOCALE_SETTINGS.getString('number_format.decimal_point');
		}
		else
		{
		   numberFormatDecimalPoint = page.bundle.getString('number_format.decimal_point');
		}
		if (trimmedVal.endsWith( numberFormatDecimalPoint )) trimmedVal += '0';
		var numFormat = '^[-]?[0-9]*(\\.[0-9]+)?$';
		var re = new RegExp( numFormat );	
    	var isValidNum = trimmedVal.search( re ) == 0;
		return isValidNum;
   },
   
   showInlineReceipt: function( message ) {
		window.location.href = viewSpreadsheetURL+'&inline_receipt_message='+message;
   },
   
   formatStudentName: function ( student ) {
		var nameTemplate = new Template(GradebookUtil.getMessage('userNameTemplate'));
		var nameData = {first:student.first, last:student.last, user:student.user};
		return nameTemplate.evaluate(nameData);
   },
   
   isIE: function () {
      return navigator.userAgent.toLowerCase().indexOf("msie") >= 0;
   },

   isNS7: function () {
      return navigator.userAgent.toLowerCase().indexOf("netscape/7") >= 0;
   },
   
   isFFonMac: function() {
      return GradebookUtil.isMac() && GradebookUtil.isFirefox();
   },
   
   isFirefox: function() {
     return (navigator.userAgent.toLowerCase().indexOf("firefox") != -1);
   },
   
   isMac: function() {
     return (navigator.userAgent.toLowerCase().indexOf("mac") != -1);
   },
   
   trimId: function( primaryKey )
   {
     if ( primaryKey.charAt(0) != '_' ) return primaryKey;
     return primaryKey.slice(1, primaryKey.lastIndexOf('_') );
   },

   getMessage: function (key) {
      if ( Gradebook.getModel() ){
      	return Gradebook.getModel().getMessage(key);
      } else {
      	// in case the model is not loaded but we are on the grid page
      	if ( gradebook2Messages && gradebook2Messages[ key ] ) return gradebook2Messages[ key ];
      	return key;
      }
   },

   getElementsComputedStyle: function ( htmlElement, cssProperty, mozillaEquivalentCSS) {
      if ( arguments.length == 2 )
         mozillaEquivalentCSS = cssProperty;

      var el = $(htmlElement);
      if ( el.currentStyle )
         return el.currentStyle[cssProperty];
      else
         return document.defaultView.getComputedStyle(el, null).getPropertyValue(mozillaEquivalentCSS);
   },

   toViewportPosition: function(element) {
      return this._toAbsolute(element,true);
   },

   /**
    *  Compute the elements position in terms of the window viewport
    *  so that it can be compared to the position of the mouse (dnd)
    *  This is additions of all the offsetTop,offsetLeft values up the
    *  offsetParent hierarchy, ...taking into account any scrollTop,
    *  scrollLeft values along the way...
    *
    *  Note: initially there was 2 implementations, one for IE, one for others.
    *  Mozilla one seems to fit all though (tested XP: FF2,IE7, OSX: FF2, SAFARI)
    **/
   _toAbsolute: function(element,accountForDocScroll, topParent ) {
      return this._toAbsoluteMozilla(element,accountForDocScroll,topParent);
   },

   /**
    *  Mozilla did not report all of the parents up the hierarchy via the
    *  offsetParent property that IE did.  So for the calculation of the
    *  offsets we use the offsetParent property, but for the calculation of
    *  the scrollTop/scrollLeft adjustments we navigate up via the parentNode
    *  property instead so as to get the scroll offsets...
    *
    **/
   _toAbsoluteMozilla: function(element,accountForDocScroll, topParent) {
      var x = 0;
      var y = 0;
      var parent = element;
      while ( parent && ( !topParent || parent!=topParent ) ) {
         x += parent.offsetLeft;
         y += parent.offsetTop;
         parent = parent.offsetParent;
      }

      parent = element;
      while ( parent &&
              parent != document.body &&
              parent != document.documentElement &&
              ( !topParent || parent!=topParent ) ) {
         if ( parent.scrollLeft  )
            x -= parent.scrollLeft;
         if ( parent.scrollTop )
            y -= parent.scrollTop;
         parent = parent.parentNode;
      }

      if ( accountForDocScroll ) {
         x -= this.docScrollLeft();
         y -= this.docScrollTop();
      }

      return { x:x, y:y };
   },

   docScrollLeft: function() {
      if ( window.pageXOffset )
         return window.pageXOffset;
      else if ( document.documentElement && document.documentElement.scrollLeft )
         return document.documentElement.scrollLeft;
      else if ( document.body )
         return document.body.scrollLeft;
      else
         return 0;
   },

   docScrollTop: function() {
      if ( window.pageYOffset )
         return window.pageYOffset;
      else if ( document.documentElement && document.documentElement.scrollTop )
         return document.documentElement.scrollTop;
      else if ( document.body )
         return document.body.scrollTop;
      else
         return 0;
   },

   getChildElementByClassName: function(parent, childTag, childClassName){
	var children = parent.getElementsByTagName(childTag);
	if (!children || children.length == 0) return null;
	for (var i = 0; i < children.length; i++){
		if (children[i].className.indexOf(childClassName) >= 0){
			return children[i];
		}
	}
	return null;
   },
	
   setChildElementTextByClassName: function(parent, childTag, childClassName, text){
	var child = GradebookUtil.getChildElementByClassName(parent, childTag, childClassName);
	if (child == null) return;
	child.innerHTML = text;
   },
	
	getLogger: function() {
	  	if (window.gbModel) return gbModel.getLogger(); // in case current scope owns gbModel
      	if (parent.gbModel) return parent.gbModel.getLogger();
	},

	debug: function(s) {
		var logger = this.getLogger();
		if (logger) {
			logger.debug(s);
		}
	},

	error: function(s) {
		var logger = this.getLogger();
		if (logger) {
			logger.error(s);
		}
	},
	
	//on firefox/mac scroll bars will show ontop of anything if not shimmed
    shimDiv: function( menuDiv ) {
        var shimIFrame =  $('shimDiv');
        if (!shimIFrame) return;
        shimIFrame.style.width = menuDiv.offsetWidth;
        shimIFrame.style.height = menuDiv.offsetHeight;
        var position = Position.page(menuDiv);
        shimIFrame.style.top = position[1];
        shimIFrame.style.left = position[0];
        shimIFrame.style.zIndex = 2;
        shimIFrame.style.display = "block";        
    },
    
    clearShim: function()
    {
        if ( $("shimDiv") ) $("shimDiv").style.display="none";
    }
};
