/**
  *  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:
  * Authors(s): Bill Richard
  * Description: Main controller class for gradebook2 grid.
  * Version:
  **/

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; }
}

var GradebookScrollContext = Class.create({
  initialize: function( accessibleMode, currentView ) {

	this.accessibleMode = accessibleMode;
    this.currentView = currentView;

    if ( this.accessibleMode ) {
	  this.accessibleScrollSettings = {};
	} else {
	  this.inaccessibleScrollSettings = {};
	}
  },

  saveScrollCoordinates: function() {
    if ( this.accessibleMode ) {
      this.saveScreenReaderModeScrollCoordinates();
    } else {
      this.saveInteractiveModeScrollCoordinates(theGradeCenter.grid.viewPort);
    }
  },

  saveScreenReaderModeScrollCoordinates: function() {
    var container = document.getElementById('table1_accessible_container');
	this.accessibleScrollSettings.y = container.scrollTop;
	this.accessibleScrollSettings.x = container.scrollLeft;
  },

  saveInteractiveModeScrollCoordinates: function(viewPort) {
	  if ( viewPort.scrollerDiv ) {
	    this.inaccessibleScrollSettings.scrollTop = viewPort.scrollerDiv.scrollTop;
	  }
	  if ( viewPort.scrollerDivH ) {
	    this.inaccessibleScrollSettings.scrollLeft = viewPort.scrollerDivH.scrollLeft;
	  }
	  this.inaccessibleScrollSettings.lastVScrollPos = viewPort.lastVScrollPos;
	  this.inaccessibleScrollSettings.lastHScrollPos = viewPort.lastHScrollPos;
  }
});

GradebookScrollContext.Constants = {
  GRADEBOOK_SCROLL_CONTEXT: 'GradebookScrollContext'
};

GradebookScrollContext.getNewInstance = function( model, accessibleMode ) 
{
    var gradebookScrollContext = new GradebookScrollContext( accessibleMode, model.currentView );
    model.setObject( GradebookScrollContext.Constants.GRADEBOOK_SCROLL_CONTEXT , gradebookScrollContext );
    return gradebookScrollContext;
};

GradebookScrollContext.getExistingInstance = function( model, accessibleMode ) {
	var scrollContext = model.getObject( GradebookScrollContext.Constants.GRADEBOOK_SCROLL_CONTEXT  );
	if (!scrollContext)
	{
	  return null;
	}
	if (scrollContext.currentView != model.currentView)
	{
	  return null;
	}
	if (scrollContext.accessibleMode != model.accessibleMode)
	{
	  return null;
	}
	return scrollContext;
};

Gradebook.Grid = Class.create();

Gradebook.Grid.prototype = {

   initialize: function( tableId, gradebookService, options, model) {

     this.options = {
      scrollerBorderRight: '1px solid #ababab',
      sortBlankImg: 'images/blank.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');
     this.wrapTable();
   },
   
   /**
    * Determines the html table row index of the specified course membership id.
    *
    * Returns an object with two properties: a boolean indicating whether the row was found
    * and if found, the row index of the specified course member.
    */
   getHtmlRowIndexByUserId: function(userId)
   {
     var visibleRows = this.model.visibleRows;
	 var result = {found:false};
	 var scrollableColRowIterators = this.model.getRowIterators();

	 for (var i=0; i < visibleRows.length; i++) {
	   if ( scrollableColRowIterators[i].dataArray[0].uid == userId )
	   {
	     result.index = i;
	     result.found = true;
		 break;
		}
	 }
	 return result;
   },

   wrapTable: function()
   {
     var table = this.table;
     // wrap table with a new container div: relative see IE7 bug: http://rowanw.com/bugs/overflow_relative.htm
     table.insert({before: "<div id='"+this.tableId+"_container' style='position:relative;'></div>"});
     table.previousSibling.appendChild(table);

     if (!this.options.accessibleMode)
     {
       // wrap table with a new viewport div
       table.insert({before: "<div id='"+this.tableId+"_viewport'></div>"});
       table.previousSibling.appendChild(table);
     }
   },

    // 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) {
    this.loaded=true;
    model = null;
    parent.gbModel = null;
    if (serverReply){
      // server returned error page instead of json data
        if ( exception.name && exception.message )
        {
          document.write( exception.name + ': ' + exception.message+'     '  );
        }
      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);
    this.viewPort.refreshContentsH();
    this._setAccessibilityHeaders();
    this.updateSortImage();
    this.restoreFocus();

    if ( this.options.onLoadComplete ){
      this.options.onLoadComplete();
    }

    this.loaded=true;
  },
  
  /**
   * Scrolls the viewport horizontally to display the specified grade item 
   */
  scrollGradeItemIntoViewPort: function( gradableItemId ) {
    var htmlColumnIndex = this.model.getVisibleColDefIndex( gradableItemId );
    if (htmlColumnIndex == -1) {
      return false;
    }
    var htmlColumn = theGradeCenter.grid.isHtmlColumnIndexVisible( htmlColumnIndex );
    if (!htmlColumn.found) {
      // grade column is not currently visible.  scroll horizontally to ensure it's in view
      theGradeCenter.grid.viewPort.scrollCols(htmlColumn.diff, true);
    }
    return true;
  },
  
  /**
   * Scrolls the viewport vertically to display the specified course member
   */
  scrollCourseMemberIntoViewPort: function( userId ) {
    var htmlRow = theGradeCenter.grid.getHtmlRowIndexByUserId( userId );
    if (!htmlRow) {
      return false;
    }

    if (!htmlRow.found)
    {
      return false;
    }

    htmlRow = theGradeCenter.grid.isHtmlRowIndexVisible( htmlRow.index );
    if (!htmlRow.isVisible) {
      // student row is not currently visible.  scroll vertically to ensure it's in view
      theGradeCenter.grid.viewPort.scrollRows(htmlRow.diff, true);
    }
    return true;
  },

  /**
   * Returns whether the specified domElement is currently viewable in the grid
   */
  isGridCellInView: function( domElement ) {
	var docViewTop =  document.viewport.getScrollOffsets().top;
    var docViewBottom = docViewTop + $(document).viewport.getHeight();
    var viewPortOffset = $(domElement).viewportOffset();
    var elemTop = viewPortOffset.top;
    var elemBottom = elemTop + $(domElement).getHeight();

    return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
    		(elemBottom <= docViewBottom) &&  (elemTop >= docViewTop) );
  },
  
  /**
   * Returns whether the specified row is visible in the viewport
   */
  isHtmlRowIndexVisible: function(index)
  {
    var results = {isVisible:false};
	var numRows = this.viewPort.numVisibleRows;
	var lastRowPos = this.viewPort.lastRowPos;

	if ( index < lastRowPos || index > lastRowPos + numRows ) {
		results.diff = index - lastRowPos ;
	} else {
	  results.isVisible	= true;
	}
	return results;
  },
  
  /**
   * Returns whether the specified column is visible in the viewport
   */
  isHtmlColumnIndexVisible: function(index) {
    var numFrozenColumns = this.viewPort.options.numFrozenColumns;
    var numVisibleColumns = this.viewPort.numVisibleCols;
    var colOffset = this.viewPort.colOffset;
    if ( numFrozenColumns >= numVisibleColumns ) {
      return -1;
    }
    var results = {found:false};
    var items = this.model.getColDefs(false, false);
    var lastColIndex = numVisibleColumns + colOffset - 1;
    var lastViewableColumn = items[ lastColIndex ];

    for (var i = 0, idx = 1; i < items.length; i++) {
      if ( i == index ) {
        results.index = i;
    	if ( lastColIndex > results.index ) {
    	  results.diff = results.index - lastColIndex;
    	 } else if (results.index > lastColIndex) {
    	   results.diff = results.index - lastColIndex;
    	} else {
    	   results.found = true;
    	}
    	break;
      }
    }
    return results;
  },

  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 = this.options.tableWidth;
    contDiv.style.height = h + "px";
    contDiv.style.width = w + "px";
    contDiv.style.overflow = "auto";
  },

  _initializeHTML: function() {
    this._sizeHTMLTable();
    if (this.options.accessibleMode)
    {
      this.setAccessibleViewportSize( );
      return;
    }

    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() {
    GradebookScrollContext.getNewInstance( this.model, this.options.accessibleMode ).saveScrollCoordinates();
    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() + 1 ; //in accessible mode, the same table has the header and content
      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, 10);
      if ( numRows < this.model.getMinimumRows() )
      {
        numRows = this.model.getMinimumRows() ;
      }
      if ( this.model.getNumRows() < numRows ) 
      {
        numRows = this.model.getNumRows() ;
      }
      numCols = parseInt(this.options.tableWidth/cell.offsetWidth, 10);
    }

    // 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;

    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 = " ";

  },

  getAbbrColIndexes: function()
  {
    if ( !this.abbrColIndexes )
    {
      this.abbrColIndexes = 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.abbrColIndexes[ lastNameColIndex ] = true;
        this.abbrColIndexes[ firstNameColIndex ] = true;
      }
      else if  ( userNameColIndex != -1 )
      {
        this.abbrColIndexes[ userNameColIndex ] = true;
      }
      else
      {
        this.abbrColIndexes.push[ 0 ] = true;
    }
    }
    return this.abbrColIndexes;
  },

  onDocumentClickHandler: function(evt) {
    if ( document.ignoreOnClick || Gradebook.alertLightbox ) return;
    Gradebook.CellController.prototype.closePopupsAndRestoreFocus(evt);
  },

  onDocumentKeyDownHandler: function(evt) {
    if ( Gradebook.alertLightbox ) return;
    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();
    this.updateLastModifiedTS();
    this.restoreInteractiveModeScrollCoordinates();
  },

  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;
  },

  restoreInteractiveModeScrollCoordinates: function() {
    var gradebookScrollContext = GradebookScrollContext.getExistingInstance( this.model, this.options.accessibleMode );
    if ( !gradebookScrollContext || gradebookScrollContext.accessibleMode )
    {
      return;
    }
    if ( this.scrollerDiv ) {
	  this.scrollerDiv.scrollTop = gradebookScrollContext.inaccessibleScrollSettings.scrollTop;
    }
	  this.lastVScrollPos = gradebookScrollContext.inaccessibleScrollSettings.lastVScrollPos;
    if ( this.scrollerDivH ) {
      this.scrollerDivH.scrollLeft = gradebookScrollContext.inaccessibleScrollSettings.scrollLeft;
      this.setColOffsetFromScrollOffset();
    }
    this.lastHScrollPos = gradebookScrollContext.inaccessibleScrollSettings.lastHScrollPos;
    if ( this.scrollerDiv ) {
      var contentOffset = parseInt( this.scrollerDiv.scrollTop / parseInt(this.rowHeight, 10), 10 );
      this.lastRowPos = contentOffset;
    }
  },

  modelChanged: function() {
    this.updateLastModifiedTS();
    this.refreshContentsH();
  },

  updateLastModifiedTS: function() {
    var t = this.model.lastLogEntryTS;
    if (!t) return;
	var d = new Date();
	d.setTime( t );
    var gcFrame = (top.content.gradecenterframe) ? top.content.gradecenterframe : top.content;
    $( gcFrame.document.getElementById('timeStampDiv')).update( gcFrame.LastSavedMsg + formatDate(d, "MMM dd, yyyy hh:mm a") );
  },

  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 dataCell = iterator.next();
      var htmlCell = htmlRow.cells[j+1];
      // set check box column based on isRowChecked flag for first data cell
      if (j == 0){
		var checkInput = GradebookUtil.getChildElementByClassName(htmlRow.cells[0], 'input', 'checkInput');
        checkInput.checked = dataCell.metaData.isRowChecked;
      }
      htmlCell.controller.renderHTML(dataCell);
    }
  },

  refreshContents: function(rowOffset) {
    if (this.model.getNumRows() == 0) return;
    if (this.options.accessibleMode){
      this.refreshAccessibleContents();
      return;
    }
    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;
  },


  restorePreviousAccessibleModeScrollCoordinates: function() {
    var gradebookScrollContext = GradebookScrollContext.getExistingInstance( this.model, this.options.accessibleMode );
    if ( gradebookScrollContext && gradebookScrollContext.accessibleMode ) {
	  var accessibleContainer = document.getElementById('table1_accessible_container');
	  accessibleContainer.scrollTop = gradebookScrollContext.accessibleScrollSettings.y;
	  accessibleContainer.scrollLeft = gradebookScrollContext.accessibleScrollSettings.x;
    }
  },


  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;
    var abbrColIndexes = this.grid.getAbbrColIndexes();
    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 dataCell = 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(dataCell);
        if ( abbrColIndexes[ j ] )
        {
          htmlCell.abbr = htmlCell.controller.getGridCell().getValue();
          htmlCell.scope = 'row';
        }
        // 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 = dataCell.metaData.isRowChecked;
        }
      }
      var rowTitle = GradebookUtil.getMessage( 'selectUserMsg' );
      $(htmlRow.cells[ 0 ]).down('input').title = rowTitle;

      this.refreshRowCounter++;
    }
    this.refreshRowCounter = null;
    setTimeout(this.restorePreviousAccessibleModeScrollCoordinates.bind(this), 0 );
  },

  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'), 10);
   },

  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();
    // rule of third: we have X rows to display, only Y are visible
    // and the height for the Y is visibleHeight, what should be the
    // height for all? totalHeight = ( visibleHeight / Y ) * X
    var numVisibleRows = this.table.rows.length;
    this.rowHeight = parseInt( visibleHeight / numVisibleRows, 10 );
    visibleHeight = this.rowHeight * numVisibleRows; // just in case rowHeight was rounded
    var divHeight = this.rowHeight * this.model.getNumRows();

    // 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    = "scroll";

    // create the inner div...
    this.heightDiv = document.createElement("div");
    this.heightDiv.style.width  = "1px";

    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);

     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;
        }
      }
      this.setColOffsetFromScrollOffset();
      this.refreshContentsH();
      this.lastHScrollPos = this.scrollerDivH.scrollLeft;
    }
  this.ignoreOnHscroll = false;
 },
 
 setColOffsetFromScrollOffset: function()
 {
   var offset = 0;
   if ( document.documentElement.dir == 'rtl' )  
   {
     // Subtract the max scroll left with the current one and divide with the avgColWidth
     offset = parseInt( (this.startScrollLeft - this.scrollerDivH.scrollLeft) / this.avgColWidth, 10);
   } 
   else 
   {
     offset = parseInt(this.scrollerDivH.scrollLeft / this.avgColWidth, 10);
   }
   this.colOffset = Math.min( offset, this.maxColOffset );   
 }

};

  //*********************************************************************
  //************ Gradebook.CellController *******************************
  //*********************************************************************

Gradebook.CellController = Class.create();

Gradebook.CellController.opCounter = 0; //ctx menu tag to avoid Ajax callback populating the wrong ctx menu when delayed

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._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));
          theGradeCenter.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( ) {
    if (this.isInitialized) return;
    this.isInitialized = true;
    // 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));
          theGradeCenter.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().hasContextMenuInfo(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();
    var points;
    if ( type == "student" ) {
        theGradeCenter.setMsgInTaskBar( colDef.getName() );
    } else {
        theGradeCenter.setHeaderInfoInTaskBar( colDef.getName(),
            GradebookUtil.getMessage( colDef.getType() + 'Msg' ),
            colDef.getPointsForDisplay() );
    }
  },

  setTaskbarInfo: function(gridCell) {
      if (!gridCell) {
         theGradeCenter.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 = parent.NumberFormatter.getDisplayFloat( gridCell.getPointsPossible() );
        visibileToStudents = GradebookUtil.getMessage((colDef.vis)?'isMsg':'isNotMsg');
      } catch ( ignore ) { };
        theGradeCenter.setTaskBar(gradeType,pointsPossible,primaryDisplay,visibileToStudents);
    } else {
        theGradeCenter.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.stopEdit(true /*commit*/, false /*keep focus on cell we exit*/, true /* - while Needs Grading are considered change */)){ //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, doNotIgnoreDash){
    if (!this.isEditing){
      return true;
    }
    var currentCell = this.editGridCell;
    GradebookUtil.debug('stopEdit row = '+this.row+' col = '+this.col+' commit = '+commit);
    // if the user press Return and the grade is a -, and it was a - before but obfuscated by a status indicator
    // then trigger the null mechanism; - is only accounted for Return, not when cell loses focus due to other reasons
    if (commit && ( this.hasUncommittedChanges() || ( doNotIgnoreDash && ( currentCell.needsGrading() || currentCell.attemptInProgress() ) ) ) ) {
      var inputVal = this.editInput.value;
      var validationError = currentCell.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 ( currentCell.getValue() == '-'){
          save = false;
        } else {
          if ( currentCell.isOverride() ) {
            if ( !currentCell.hasAttempts() ) {
              save = confirm( GradebookUtil.getMessage( 'confirmRevertMsg' ) );
            } else {
              this.showRevertWithAttemptsConfirmation( doNotSetFocus );
              return false;
            }
          } else if ( currentCell.hasMultipleAttempts() ) {
            this.showMultipleAttemptsNullConfirmation( doNotSetFocus );
            return false;
          } else {
            save = confirm(GradebookUtil.getMessage( 'confirmNullMsg' ));
          }
        }
      }
      if (save){
        // send update to server
        this.editGridCell.update(inputVal);

        // morph on timestamp on grade information bar
        this.afterSavingItem();
      }
    }
    this.returnCellToNotEdit( doNotSetFocus );
    return true;
  },

  returnCellToNotEdit: function( doNotSetFocus ) {
    this.hideValidationError();
    this.isEditing = false;
    this.editGridCell = null;
    this.viewDiv.style.display = "block";
    if (!doNotSetFocus){
      this.titleAnchor.focus();
    }
    this.editDiv.style.display = "none";
  },

  showRevertWithAttemptsConfirmation: function( doNotSetFocus ) {
    var lightboxParam = {
        dimensions: { w:600, h:240 },
        title: gridMessages.confirmRevertLBTitle,
        closeOnBodyClick: false,
        showCloseLink: false,
        contents: { id: 'revertWithAttemptsConfirm', move: true }
    }
    var revertToNeedsGradingEl = $('revertAndNeedsGradingEl');
    if ( this.editGridCell.colDef.isManual() ) {
      //reverting grades to needs grading for manual question does not make sense since there is no attempt page
      revertToNeedsGradingEl.hide();
      $('revertAndNeedsGrading').name='xxxxxx'; //fix KB navigation using arrows to move up/down the choices
    } else {
      revertToNeedsGradingEl.show();
      $('revertAndNeedsGrading').name='revertWithAttemptChoice';
    }
    Gradebook.alertLightbox = new lightbox.Lightbox( lightboxParam );
    var submitButton = $('revertWithAttemptsConfirmSubmit');
    var firstRadio = $('revertOnly');
    firstRadio.checked = true;
    Gradebook.alertLightbox.open(
        function( ) {
          firstRadio.focus();
          Gradebook.alertLightbox.lastLink = submitButton;
          Gradebook.alertLightbox.firstLink = firstRadio; } );
    var currentCell = this;
    $('revertWithAttemptsConfirmCancel').onclick = function()
    {
      currentCell.closeAlertLightbox( doNotSetFocus );
      return false;
    };
    $('revertWithAttemptsConfirmForm').onsubmit = function()
    {
      if ( $('revertOnly').checked ) {
        currentCell.editGridCell.update('-');
      } else if ( $('revertAndNeedsGrading').checked ) {
        currentCell.editGridCell.clearAll( false );
      } else if ( $('revertAndDelete').checked ) {
        if ( confirm( gridMessages.confirmDeleteOnRevert ) ) currentCell.editGridCell.clearAll( true );
      }
      // morph on timestamp on grade information bar
      currentCell.afterSavingItem();
      currentCell.closeAlertLightbox( doNotSetFocus );
      return false;
    };
  },

  showMultipleAttemptsNullConfirmation: function( doNotSetFocus ) {
    var lightboxParam = {
        dimensions: { w:640, h:420 },
        title: gridMessages.confirmRevertLBTitle,
        closeOnBodyClick: false,
        showCloseLink: false,
        contents: { id: 'multipleAttemptsNullGradeConfirm', move: true }
    }
    var templateForAttemptEl = $('attemptChoicesTemplate');
    if ( !templateForAttemptEl ) alert( 'could not find template' );
    templateForAttemptEl.hide();
    var templateForAttempt = templateForAttemptEl.down('ol').innerHTML;
    var attemptsContainer = $('attemptChoices').down('ol');
    attemptsContainer.innerHTML = '';
    attemptsContainer.forGrade = this.editGridCell.getKey();
    this.editGridCell.loadAttemptsInfo( function( cell ) {
      // 1st since this is asynchronous let's make sure the displayed
      // lightbox is still for the grade we just got the attempts for
      if ( !cell.getKey() == attemptsContainer.forGrade ) return;
      var innerHtml = '';
      cell.data.attemptsInfo.each( function( attemptInfo ) {
        var checkboxHtml = templateForAttempt;
        checkboxHtml = checkboxHtml.replace( 'ATTEMPT_ID', attemptInfo.id );
        checkboxHtml = checkboxHtml.replace( 'ATTEMPT_ID_NAME', attemptInfo.getText() );
        checkboxHtml = checkboxHtml.replace( /attemptsToClearTEMPLATE/g, 'attemptsToClear' );
        checkboxHtml = checkboxHtml.replace( /attemptsToClearIDTEMPLATE/g, 'att_' + attemptInfo.id );
        innerHtml += checkboxHtml;
      } );
      attemptsContainer.innerHTML = innerHtml;
    } );
    var templateString = templateForAttempt.innerHTML;
    $('multipleAttemptsNeedsGrading').checked=true;
    $('multipleAttemptsAll').checked=true;
    var synCheckboxesDisabledState = function() {
      var disabled = !$('multipleAttemptsSelected').checked;
      var attemptsCB = $('multipleAttemptsNullGradeConfirmForm').getInputs( 'checkbox', 'attemptsToClear' );
      attemptsCB.each( function( cb ) { cb.disabled = disabled; } );
    };
    $('multipleAttemptsAll').onclick = synCheckboxesDisabledState;
    $('multipleAttemptsSelected').onclick = synCheckboxesDisabledState;
    Gradebook.alertLightbox = new lightbox.Lightbox( lightboxParam );
    var submitButton = $('multipleAttemptsNullGradeConfirmSubmit');
    var firstRadio = $('multipleAttemptsNeedsGrading');
    firstRadio.checked = true;
    Gradebook.alertLightbox.open(
        function( ) {
          firstRadio.focus();
          Gradebook.alertLightbox.lastLink = submitButton;
          Gradebook.alertLightbox.firstLink = firstRadio; } );
    var currentCell = this;
    $('multipleAttemptsNullGradeConfirmCancel').onclick = function()
    {
      currentCell.closeAlertLightbox( doNotSetFocus );
      return false;
    };
    $('multipleAttemptsNullGradeConfirmForm').onsubmit = function()
    {
      var form = $('multipleAttemptsNullGradeConfirmForm');
      if ( $('multipleAttemptsSelected').checked ) {
        var attemptIds = new Array();
        var attemptsCB = form.getInputs( 'checkbox', 'attemptsToClear' );
        attemptsCB.each( function( cb ) { if ( cb.checked ) attemptIds.push( cb.value );} );
      } else {
        var isAllAttempt = (  $('multipleAttemptsAll').checked )
      }
      if ( $( 'multipleAttemptsNeedsGrading').checked )
      {
        if ( isAllAttempt ) {
          currentCell.editGridCell.clearAll( false );
        } else if ( attemptIds && attemptIds.size() > 0 ) {
          currentCell.editGridCell.clearSelected( attemptIds, false );
        }
      } else if ( $( 'multipleAttemptsDelete').checked  ) {
        if ( isAllAttempt ) {
          if ( confirm( gridMessages.confirmDeleteOnRevert ) ) currentCell.editGridCell.clearAll( true );
        } else if ( attemptIds && attemptIds.size() > 0 ) {
          if ( confirm( gridMessages.confirmDeleteSelected ) ) currentCell.editGridCell.clearSelected( attemptIds, true );
        }
      }
      currentCell.closeAlertLightbox( doNotSetFocus );
      return false;
    };
  },

  closeAlertLightbox: function( doNotSetFocus ) {
    this.returnCellToNotEdit( doNotSetFocus );
    this.grid.selectRelativeCell(1, 0);
    Gradebook.alertLightbox.close();
    Gradebook.alertLightbox = null;
  },

  hasUncommittedChanges: function(evt){
    return (this.isEditing && this.editInput.value != this.editGridCell.getEditValue() );
  },

  //************ rendering logic *******************************

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

  renderHTML: function(dataCell) {
    var anchorVal;
    var altVal;
    if (!this.gridCell)
    {
      this.gridCell = new parent.Gradebook.GridCell();
      if (!this.grid.options.accessibleMode && this.col == 1){
        // checkbox column shares grid controller with first cell, this allows access
        // to metadata for select row functionality
        this.previousElementSibling(this.htmlCell).controller.gridCell = this.gridCell;
      }
    }
    var gridCell = this.gridCell;
    gridCell.setData( dataCell );
    if (this.grid.options.accessibleMode)
    {
      this._accessibleInit();
    }
    this.htmlCell.className = this.grid.model.getColorScheme( gridCell );

    if (gridCell.isExcluded()){
      anchorVal = gridImages.excludedGrade;
    } 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.isOverride()){
        anchorVal = gridImages.gradeOverride+" "+anchorVal;
      }
      if (anchorVal.blank()){
        anchorVal = '&nbsp;'
      }
      if (this.titleAnchor){
        this.titleAnchor.innerHTML = anchorVal;
        this.titleAnchor.title = altVal;
      }
    }
  },

  renderHeaderCellHTML: function( colDef ) {
    if (this.grid.options.accessibleMode)
    {
      this._accessibleInit();
    }
    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;
  },

  afterSavingItem: function(){
    var timeStampDiv = $("timeStampDiv");
    var flashingColor = "color:#000;background:#fff";
    var backgroundColor = "color: #000; background: #FFCC66";
    if ( timeStampDiv  != null && timeStampDiv  != undefined) {
      timeStampDiv.morph( backgroundColor );
      setTimeout("$('timeStampDiv').morph('"+flashingColor+"')", 1000);
    }
  },
  showValidationError: function(error) {
    var errDiv = $("errorDiv");
    var p = errDiv.down('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.col == 0 ){
      return false;
    } else {
      return (this.getGridCell().hasContextMenuInfo(this) );
    }
  },

  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 {
      theGradeCenter.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) {
      // OP Counter so that AJAX call back does not affect ctx menu if not the one issuing the request
    this.contextMenuId = menuInfo.id;
    var menuDiv = $(this.contextMenuId);
    menuDiv.opCounter = ++Gradebook.CellController.opCounter;
    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);
      if (!itemLink)
      {
        return;
      }
      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( GradebookUtil.getMessage( mi.receipt ) );
            }
        }
        Event.observe(mi.id, 'click', itemLink.onclickHandler);
      }
      if (!firstItem) firstItem = itemLink;
      lastItem = itemLink;
    }.bind(this));
    // Dynamic Items are not in the template DIV and needs to be added each time the menu is displayed
    if ( menuDiv.dynItemsId ) menuDiv.dynItemsId.each( function(id) { var el = $(id); if ( el ) el.remove(); } );
    if ( menuInfo.dynItems && menuInfo.dynItems.length > 0 ) {
      menuDiv.dynItemsId = new Array();
      GradebookUtil.addDynItemsToContextMenu( menuDiv, menuInfo.dynItems );
    }
    if ( menuInfo.dynAppender ) {
      var currentOPCounter = menuDiv.opCounter;
      var appendToMenuCallback = function( cell ) {
        if ( menuDiv.opCounter != currentOPCounter ) return; // not the same ctx menu anymore...
        GradebookUtil.addDynItemsToContextMenu( menuDiv, cell.getMenuDynItems() );
      }
      menuInfo.dynAppender( appendToMenuCallback );
    }
    // 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] + Element.getWidth( this.contextMenuAnchor ) - width;
      if ( offset[0] < 0 ) offset[0]= 0;
    }
    if ( offset[0] + width > bodyWidth )
    {
      offset[0] = offset[0] - width + Element.getWidth( this.contextMenuAnchor );
    }
    var ypos = offset[1] + Element.getHeight( this.contextMenuAnchor );
    menuDiv.setStyle({ left: offset[0] + "px", top: ypos + "px"});
    if ( GradebookUtil.isFFonMac() ) GradebookUtil.shimDiv( menuDiv );
    (function() { if (menuDiv.style.display != 'none') 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
    theGradeCenter.instructorCommentsResize._reset();
    theGradeCenter.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() {
    var instructorNotes = $("instructorComments");
    var studentComments = $("studentComments");
    if ( !GradebookUtil.validateMaxLength( instructorNotes, GradebookUtil.getMessage( 'instructorNotesMsg' ), 1000 ) )
    {
        return false;
    }
    if ( !GradebookUtil.validateMaxLength( studentComments, GradebookUtil.getMessage( 'feedBackToUserMsg' ), 1000 ) )
    {
        return false;
    }
    this.getGridCell().setComments(studentComments.value, instructorNotes.value);
    this.afterSavingItem();
    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 ( $("commentsDiv").getStyle("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.gridCell;
  },

  getHeaderGridCell: function(){
    return this.grid.viewPort.getHeaderGridCell(this.col);
  },

  closePopupsAndRestoreFocus: function(evt) {
    var ctrl = Gradebook.CellController.prototype;
    ctrl.closePopups(evt);

    var eventTarget = null;
    if ( evt )
      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 )
  {
    // temporarily show the context menu so we can calculate the position.
    var cmDiv = $("gradeHeaderCM")
    cmDiv.setStyle({display:'block'});
    var linkElement = $('gh_clearAllAttempts');
    var cmOffset = cmDiv.cumulativeOffset( );
    var offset = linkElement.cumulativeOffset( );
    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: ( cmOffset[1] - Gradebook.Grid.pageHeightOffset ) + "px",
          display: "block"
        });
    // hide the menu again.
    cmDiv.setStyle({display:'none'});
    // 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( GradebookUtil.getMessage( '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 );
  },

  showAlignments: function( colDef )
  {
    align.showLightbox( 'blackboard.platform.gradebook2.GradableItem;_'+ colDef.id + '_1', colDef.name.unescapeHTML(), courseID );
  },

    showRubrics: function( colDef )
    {
      rubricModule.showCourseRubrics( '_' + colDef.id + '_1', courseID, colDef.name.unescapeHTML() );
    },

  sendEmail: function(type,studentIds){
    var sendEmailFunc = this.grid.options.sendEmailFunc;
    if (sendEmailFunc)
    {
      sendEmailFunc(type,studentIds);
    }
  },

  onGridScroll: function(){
    this.closePopups();
    this.unselectCurrentCell();
  }

};


var GradebookUtil = {

   addDynItemsToContextMenu: function( menuDiv, dynItems ) {
   if ( !menuDiv.dynItemsId ) menuDiv.dynItemsId = new Array();
   for ( var i=0; i<dynItems.length; ++i ) {
    var templateElement = $( dynItems[ i ].id );
    var newMenuItem = templateElement.cloneNode( false );
    newMenuItem.id = dynItems[ i ].id + "_" + i;
    var innerHTML = templateElement.innerHTML;
    innerHTML = innerHTML.replace( dynItems[ i ].id + "_name", dynItems[ i ].name );
    newMenuItem.innerHTML = innerHTML;
    templateElement.insert( {after: newMenuItem } );
    newMenuItem.firstDescendant().onclick = dynItems[ i ].onclick;
    newMenuItem.show();
    menuDiv.dynItemsId.push( newMenuItem.id );
   }
   },

   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;
   },

  // returns true if the text area length is less than maxLength.
  // text area length is greater than maxLength, alerts user, sets focus to text area and returns false
  validateMaxLength : function( textArea, label, maxlength ) {
    var textLength = textArea.value.length;
    if ( maxlength < textLength )
    {

        if ( (textLength - maxlength) > 1 )
        {
            alert(JS_RESOURCES.getFormattedString('validation.maximum_length.plural',
                                                  new Array(label,maxlength,(textLength - maxlength))));
        }
        else
        {
            alert(JS_RESOURCES.getFormattedString('validation.maximum_length.singular',
                                                  new Array(label,maxlength)));
        }
        textArea.focus();
        return false;
    }
    else
    {
      return true;
    }
  },

   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 {
        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;
   },

  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";
    }
};
