
// Namespace for m_table releated objects
var m = { "version":"1", "sorters":{}, "debug":true };

// table prototype
m.table = function( id ) {
  // table structure
  this.id = id;
  this.table = $(id);
  this.thead = null;
  this.tbody = null;
  this.tfoot = null;
  this.cols = [];
  this.rows = [];
  // sorting
  this.sortColumn = -1;
  this.lastSortColumn = -1;
  this.sortDirection = "down";  // vs. up
  // table select
  this.tableSelector = null;
  // col select
  this.columnSelectors = [];
  this.selectedColumns = [];
  this.lastColumnSelected = null;
  // row select
  this.rowSelectors = [];
  this.selectedRows = [];
  this.lastRowSelected = null;
  // export button
  this.exporter = null;
  // add and cell forms from footer
  this.addForm = null;
  this.cellForms = [];
  this.multiActionForm = null;
  
  // event handlers
  this.sortClick = function( e ) {
    e.stop();
    var target = e.target().id;
    m.log( "Select table sgnalled by",e,"on",target);
    this.sortColumn = findValue( this.cols, target );
    if ( this.sortColumn == -1 ) {
      m.log("Unknown sort target...");
    }
    // change direction?
    if ( this.lastSortColumn!=-1 && this.lastSortColumn==this.sortColumn ) {
      if ( this.sortDirection=="down" ) {
        this.sortDirection = "up";
      }
      else {
        this.sortDirection = "down";
      }
    }
    this.sortTable();
    // last sort
    this.lastSortColumn = this.sortColumn;
    m.deselect();
  }
  
  this.selectTableClick = function( e ) {
    e.stop();
    var target = e.target().id;
    m.log( "Select table sgnalled by",e,"on",target);
    if ( this.selectedColumns.length > 0 || this.selectedRows.length > 0 ) {
      this.deselectTable();
    }
    else {
      this.selectTable();
    }
    m.deselect();
  }
  
  this.selectColumnClick = function( e ) {
    e.stop();
    var target = e.target().id;
    m.log( "Select column sgnalled by",e,"on",target);
    // check multiselect
    var isMulti = false;
    if ( this.lastColumnSelected && this.lastColumnSelected!=target ) {
      isMulti = e.modifier().shift;
    }
    // check selection state to apply -- -1 means select, anything else means deselect
    var isSelected = -1;
    if ( isMulti ) {
      // if last column is not selected, then pretend target is selected so deselect is called
      if ( findValue(this.selectedColumns, this.lastColumnSelected) == -1 ) {
        isSelected = 1;
      }
    }
    else {
      isSelected = findValue(this.selectedColumns, target);
    }
    // find start and end of selection op
    var colStart = colEnd = findValue( this.columnSelectors, target );
    if ( isMulti ) {
      colStart = findValue( this.columnSelectors, this.lastColumnSelected );
      if ( colStart > colEnd ) {
        var temp = colStart;
        colStart = colEnd;
        colEnd = temp;
      }
    }
    m.log("isSelected:",isSelected," isMulti:",isMulti," colStart:",colStart," colEnd:",colEnd);
    // do selection
    if ( isSelected == -1 ) {
      for( var i=colStart; i<=colEnd; i++ ) {
        this.selectTableColumn( i );
      }
    }
    // or deselection
    else {
      for( var i=colStart; i<=colEnd; i++ ) {
        this.deselectTableColumn( i );
      }
    }
    // last selected
    this.lastColumnSelected = target;
    m.deselect();
  }
  
  // row selection event handler
  this.selectRowClick = function( e ) {
    e.stop();
    var target = e.target().id;
    m.log( "Select row sgnalled by",e,"on",target);
    // check multiselect
    var isMulti = false;
    if ( this.lastRowSelected && this.lastRowSelected!=target ) {
      isMulti = e.modifier().shift;
    }
    // check selection state to apply -- -1 means select, anything else means deselect
    var isSelected = -1;
    if ( isMulti ) {
      // if last column is not selected, then pretend target is selected so deselect is called
      if ( findValue(this.selectedRows, this.lastRowSelected) == -1 ) {
        isSelected = 1;
      }
    }
    else {
      isSelected = findValue(this.selectedRows, target);
    }
    // find start and end of selection op
    var rowStart = rowEnd = findValue( this.rowSelectors, target );
    if ( isMulti ) {
      rowStart = findValue( this.rowSelectors, this.lastRowSelected );
      if ( rowStart > rowEnd ) {
        var temp = rowStart;
        rowStart = rowEnd;
        rowEnd = temp;
      }
    }
    m.log("isSelected:",isSelected," isMulti:",isMulti," rowStart:",rowStart," rowEnd:",rowEnd);
    // do selection
    if ( isSelected == -1 ) {
      for( var i=rowStart; i<=rowEnd; i++ ) {
        this.selectTableRow( i );
      }
    }
    // or deselection
    else {
      for( var i=rowStart; i<=rowEnd; i++ ) {
        this.deselectTableRow( i );
      }
    }
    // last selected
    this.lastRowSelected = target;
    m.deselect();
  }
  
  
  this.collapseColumnClick = function( e ) {
    e.stop();
    var col = e.src().parentNode;
    // colgroup?
    var prefix = this.table.id+"_colselect";
    var label = col.id.substr( prefix.length );
    var colgroupid = this.table.id+"_colgroup_"+label;
    log("Collapse column",col.id,colgroupid);
    if ( hasElementClass( $(colgroupid), "collapsed" ) ) {
      removeElementClass( $(colgroupid), "collapsed" );
    }
    else {
      addElementClass( $(colgroupid), "collapsed" );
      var i = this.getColumnIndex( label );
      this.deselectTableColumn( i );
    }
    //introspect( $(colgroupid) );
  }
  
  this.exportClick = function( e ) {
    e.stop();
    m.log("Export button clicked");
    //m.introspect(e.mouse().page);
    var coords = e.mouse().page;
    this.tableExport( coords.x, coords.y );
  }
  
  this.addFormClick = function( e ) {
    m.log("Add form click");
  }
  
  this.addFormFocus = function( e ) {
    m.log("Add row focused");
  }
  
  this.addFormBlur = function( e ) {
    m.log("Add row lost focus");
  }
  
  this.addFormChange = function( e ) {
    m.log("** Add row changed");
    this.addForm.status = "dirty";
  }
  
  this.addFormCancelClick = function( e ) {
    m.log("Add row Cancel button clicked");
    this.addFormCancel();
  }
  
  this.addFormSubmitClick = function( e ) {
    m.log("Add row Submit button clicked");
  }
  
  this.editCellClick = function( e ) {
    e.stop();
    m.log("Edit cell clicked",e.target());
    var targetcell = e.target();
    while ( targetcell.tagName!='TD' && targetcell!=e.src() ) {
      targetcell = targetcell.parentNode;
    }
    if ( targetcell==e.src() ) {
      log("Unable to determine cell clicked.");
      return false;
    }
    this.editFormDisplay( targetcell.id );
  }
  
  this.editFormKeydown = function( e ) {
    var kcode = e.key().code;
    var kshift = e.modifier().shift;
    if ( kcode==9 || kcode==25 ) {
      if (kshift ) {
        e.target().stopkey = "stab";
      }
      else {
        e.target().stopkey = "tab";
      }
      
    }
    else if ( kcode==13 ) {
      if ( !kshift ) {
        e.target().stopkey = "enter";
      }
      else {
        e.target().stopkey = "senter";
      }
    }
  }
  
  this.editFormKeypress = function( e ) {
    var changeFocus = false;
    if ( e.target().stopkey ) {
      changeFocus = e.target().stopkey;
      if ( changeFocus=="senter" && e.target().tagName=="TEXTAREA" ) {
        changeFocus = false;
      }
      if ( changeFocus ) {
        log("Stopped keypress");
        e.stop();
        e.target().blur();
      }
      else {
        log("Keypress allowed",e.target().stopkey);
      }
      e.target().stopkey = false;
    }
    else if ( e.key().code==25 ) {
      e.stop();
      log("Stopped Safari shift-tab");
      changeFocus = "stab";
    }
    if ( changeFocus ) {
      m.log(changeFocus,"pressed to change focus");
      var cellid = this.id+"_"+e.target().id.substr( this.id.length + 9 );
      switch ( changeFocus ) {
        case "tab":
          this.editCellNext( cellid, 1 );
          break;
        case "stab":
          this.editCellNext( cellid, -1 );
          break;
        case "enter":
          this.editRowNext( cellid, 1 );
          break;
        case "senter":
          this.editRowNext( cellid, -1 );
          break;
      }
    }
  }
  
  this.editFormFocus = function( e ) {
    m.log("Edit cell focused");
  }
  
  this.editFormBlur = function( e ) {
    e.stop();
    var target = e.target();
    // strip off prefix_editForm
    var cellid = this.id+"_"+target.id.substr( this.id.length + 9 );
    m.log("Edit cell lost focus",target.id,target.status,"for cell",cellid);
    if ( target.status=="clean" ) {
      this.editFormCancel( cellid );
    }
    else {
      this.editFormSubmit( cellid );
    }
  }
  
  this.editFormChange = function( e ) {
    m.log("** Edit form changed at",e.target().id);
    e.target().status = "dirty";
  }
  
  this.editFormCancelClick = function( e ) {
    m.log("Edit cell Cancel button clicked");
  }
  
  this.editFormSubmitClick = function( e ) {
    m.log("Edit cell Submit button clicked");
  }
  
  
  this.multiActionFormSubmit = function( e ) {
    log("multi-action form submit with selected rows",this.selectedRows);
    if ( this.selectedRows.length < 1 ) {
      alert("Nothing selected");
      e.stop();
      return;
    }
    // get selected ids
    for( i=0;i<this.selectedRows.length; i++ ) {
      var row = $( this.selectedRows[i] );
      this.multiActionForm.appendChild( INPUT({"type":"hidden", "name":"select["+row.title+"]", "value":row.title}) );
      log("selected",row.title);
    }
  }
  
  
  /****** INIT FUNCTION STARTS HERE **************/
  
  // start parsing
  if ( !this.table ) {
    throw new Error( "No element matches id "+id );
  }
  if ( this.table.tagName.toLowerCase() != "table" ) {
    throw new Error( "Element "+id+" is a "+this.table.tagName.toLowerCase()+" not a table" );
  }
  m.log("Found m_table","#"+this.id,this.table);
  // thead and tbody and tfoot
  var theads = this.table.getElementsByTagName("thead");
  if ( theads.length != 1 ) {
    throw new Error( "Table must have exactly one thead element, and this one has "+theads.length );
  }
  this.thead = theads.item(0);
  var tbodys = this.table.getElementsByTagName("tbody");
  if ( tbodys.length != 1 ) {
    throw new Error( "Table must have exactly one tbody element, and this one has "+tbodys.length );
  }
  this.tbody = tbodys.item(0);
  var tfoots = this.table.getElementsByTagName("tfoot");
  if ( tfoots.length > 0 ) {
    this.tfoot = tfoots.item(0);
  }
  
  // columns
  var cols = this.thead.getElementsByTagName("th");
  if ( cols.length < 2 ) {
    throw new Error( "Table header must include at least two columns. This one has "+cols.length );
  }
  if ( hasElementClass(cols[0], "export") ) {
    this.exporter = cols[0].id;
    connect($(this.exporter),"onclick",this,"exportClick");
    m.log("Found export elelemt", this.exporter);
  }
  for( var i=1; i<cols.length; i++ ) {
    this.cols.push( cols.item(i).id );
    connect(cols.item(i),"onclick",this,"sortClick");
  }
  m.log("Found column headers",this.cols);
  // column selectors
  var colsels = getElementsByTagAndClassName("td","selector",this.thead);
  if ( hasElementClass(colsels[0], "tableselect") ) {
    this.tableSelector = colsels[0].id;
    connect($(this.tableSelector),"onclick",this,"selectTableClick");
    m.log("Found table selector", this.tableSelector);
  }
  for( var i=1; i<colsels.length; i++ ) {    
    this.columnSelectors.push( colsels[i].id );
    connect($(colsels[i].id),"onclick",this,"selectColumnClick");
  }
  m.log("Found column selectors", this.columnSelectors);
  // collumn collapsers
  var colcollapsers = getElementsByTagAndClassName("span","collapser",this.thead);
  if ( colcollapsers ) {
    for( var i=0; i<colcollapsers.length; i++ ) {
      connect( colcollapsers[i],"onclick",this,"collapseColumnClick");
    }
  }
  m.log("Found column collapsers", colcollapsers);
  /**
  // add and cell forms
  var addforms = getElementsByTagAndClassName("tr","addform",this.thead);
  if ( addforms[0] ) {
    this.addForm = addforms[0];
    this.addForm.status = false;
    this.addForm.style.display = 'none';
    m.log("Found add form",this.addForm);
  }
  var cellforms = getElementsByTagAndClassName("*","m_formelement",this.addForm);
  for( var i=0; i<cellforms.length; i++ ) {
    this.cellForms.push( cellforms[i].id );
    connect($(cellforms[i].id),"onfocus",this,"addFormFocus");
    connect($(cellforms[i].id),"onblur",this,"addFormBlur");
    connect($(cellforms[i].id),"onchange",this,"addFormChange");
  }
  m.log("Found cell forms", this.cellForms );
  connect($(this.id + "_formAction"), "onclick", this, "addFormSubmitClick");
  connect($(this.id + "_formCancel"), "onclick", this, "addFormCancelClick");
  **/
  // rows
  var rows = this.tbody.getElementsByTagName("tr");
  for( var i=0; i<rows.length; i++ ) {
    connect($(rows.item(i).id),"ondblclick",this,"editCellClick");
    this.rows.push( rows.item(i).id );
  }
  m.log("Found rows",this.rows);
  // row selectors
  var rowsels = getElementsByTagAndClassName("td","selector",this.tbody);
  for( var i=0; i<rowsels.length; i++ ) {
    this.rowSelectors.push( rowsels[i].id );
    connect($(rowsels[i].id),"onclick",this,"selectRowClick");
  }
  m.log("Found row selectors", this.rowSelectors);
  // action form
  this.multiActionForm = $( this.id + "_multiActionForm" );
  if ( this.multiActionForm ) {
    connect( this.multiActionForm, "onsubmit", this, "multiActionFormSubmit" );
    log("Multi-action form",this.multiActionForm.id,"with inputs",this.multiActionInputs);
  }
  else {
    log("No multi-action form found.");
  }

}



// table selection method
m.table.prototype.selectTable = function() {
  log("Selecting all ",this.cols.length,"columns in all",this.rows.length,"rows");
  addElementClass( $(this.tableSelector), "selected" );
  for( var i=0; i<this.columnSelectors.length; i++ ) {
    var col = this.columnSelectors[i];
    if ( findValue(this.selectedColumns, col)==-1 ) {
      this.selectedColumns.push(col);
      //m.log("Selected is now",this.selectedColumns);
    }
    addElementClass($(col),"selected");
  }
  for( var rx=0; rx<this.rows.length; rx++ ) {
    this.selectTableRow( rx, true );
  }
  this.tableSelected = true;
}

// table deselection method
m.table.prototype.deselectTable = function() {
  log("Deselecting entire table");
  removeElementClass( $(this.tableSelector), "selected" );
  for( var i=0; i<this.columnSelectors.length; i++ ) {
    var col = this.columnSelectors[i];
    var pos = findValue(this.selectedColumns, col);
    if ( pos !=-1 ) {
      var foo = this.selectedColumns.splice( pos, 1 );
      //m.log("Selected is now",this.selectedColumns,"removed was",foo);
    }
    removeElementClass($(col),"selected");
  }
  for( var rx=0; rx<this.rows.length; rx++ ) {
    this.deselectTableRow( rx, true );
  }
  this.tableSelected = false;
}

// column selection method
m.table.prototype.selectTableColumn = function( index ) {
  log("Selecting column",index+1,"in all",this.rows.length,"rows");
  var col = this.columnSelectors[index];
  if ( findValue(this.selectedColumns, col)==-1 ) {
    this.selectedColumns.push(col);
    //m.log("Selected is now",this.selectedColumns);
  }
  addElementClass($(col),"selected");
  for ( var r=0; r<this.rows.length; r++ ) {
    cols = $(this.rows[r]).getElementsByTagName("td");
    addElementClass( cols.item(index+1), "colselected" );
  }
}

// column deselection method
m.table.prototype.deselectTableColumn = function( index ) {
  log("Deselecting column",index+1,"in all",this.rows.length,"rows");
  var col = this.columnSelectors[index];
  var pos = findValue(this.selectedColumns, col);
  if ( pos !=-1 ) {
    var foo = this.selectedColumns.splice( pos, 1 );
    //m.log("Selected is now",this.selectedColumns,"removed was",foo);
  }
  removeElementClass($(col),"selected");
  for ( var r=0; r<this.rows.length; r++ ) {
    cols = $(this.rows[r]).getElementsByTagName("td");
    removeElementClass( cols.item(index+1), "colselected" );
  }
}

// row selection method
m.table.prototype.selectTableRow = function( index, columnsToo ) {
  log("Selecting all columns in row",index);
  var row = this.rowSelectors[index];
  if ( findValue(this.selectedRows, row)==-1 ) {
    this.selectedRows.push(row);
    //m.log("Selected is now",this.selectedRows);
  }
  addElementClass($(row),"selected");
  cols = $(this.rows[index]).getElementsByTagName("td");
  // skip first column
  for ( var c=1; c<cols.length; c++ ) {
    addElementClass( cols.item(c), "rowselected" );
    if ( columnsToo ) {
      addElementClass( cols.item(c), "colselected" );
    }
  }
  if ( this.multiActionForm && ( this.multiActionForm.style.display=="none" || !this.multiActionForm.style.display ) ) {
    this.multiActionForm.style.display = "block"
    log("Multiform",this.multiActionForm,"displayed");
  }
}

// row deselection method
m.table.prototype.deselectTableRow = function( index, columnsToo ) {
  log("Deselecting all columns in row",index);
  var row = this.rowSelectors[index];
  var pos = findValue(this.selectedRows, row);
  if ( pos !=-1 ) {
    var foo = this.selectedRows.splice( pos, 1 );
    //m.log("Selected is now",this.selectedRows,"removed was",foo);
  }
  removeElementClass($(row),"selected");
  cols = $(this.rows[index]).getElementsByTagName("td");
  // skip first column
  for ( var c=1; c<cols.length; c++ ) {
    removeElementClass( cols.item(c), "rowselected" );
    if ( columnsToo ) {
      removeElementClass( cols.item(c), "colselected" );
    }
  }
}


// table sorting method
m.table.prototype.sortTable = function() {
  log("Will sort table by column",this.sortColumn,"in the",this.sortDirection,"direction");
  if ( this.lastSortColumn!=-1 ) {
    removeElementClass( this.cols[ this.lastSortColumn ], "sorted" );
    removeElementClass( this.cols[ this.lastSortColumn ], "down" );
    removeElementClass( this.cols[ this.lastSortColumn ], "up" );
  }
  addElementClass( this.cols[ this.sortColumn ], "sorted" );
  addElementClass( this.cols[ this.sortColumn ], this.sortDirection );
  //m.introspect(this.rows,"before sort");
  window.sortColumn = this.sortColumn;
  this.rows.sort( m.sorters.generic );
  if ( this.sortDirection=="up" ) {
    this.rows.reverse();
  }
  //m.introspect(this.rows,"after sort");
  var tbody = $(this.tbody);
  for ( var r=0; r<this.rows.length; r++ ) {
    var node = tbody.removeChild( $(this.rows[r]) );
    tbody.appendChild( node );
  }
  // redo the rowselectors
  this.rowSelectors = [];
  var rowsels = getElementsByTagAndClassName("td","selector",this.tbody);
  for( var i=0; i<rowsels.length; i++ ) {
    this.rowSelectors.push( rowsels[i].id );
  }
  // redo selected rows
  if ( this.selectedRows.length > 1 ) {
    m.log("Selected rows before:",this.selectedRows)
    this.selectedRows = [];
    var selrows = getElementsByTagAndClassName("td","selected",this.tbody);
    for( var i=0; i<selrows.length; i++ ) {
      this.selectedRows.push( selrows[i].id );
    }
    m.log("Selected rows after:",this.selectedRows)
  }
}


m.table.prototype.addFormDisplay = function() {
  if ( !this.addForm.status ) {
    this.addForm.status = "clean";
    // if one row selected, fill editForms from it. if multiple rows are selected, fill with matching values only.
    // if no columns are selected, use values from all columns, otherwise use values from selected columns only.
    if ( this.selectedRows.length > 0 ) {
      this.addForm.status = "custom";
      var cellValues = [];
      var selcols = false;
      if ( this.selectedColumns.length>1 ) {
        selcols = [];
        for( var s=0; s<this.selectedColumns.length; s++ ) {
          selcols[ s ] = this.getSelectedColumnId( s );
        }
        var selcolsearch = selcols.toString() + ",";
        m.log("Selected column indexes are",selcols.toString());
      }
      for( var r=0; r<this.selectedRows.length; r++ ) {
        var row = this.getSelectedRowId( r );
        for( var i=0; i<this.cols.length; i++ ) {
          if ( cellValues[i]=="" ) continue;
          var col = this.getColumnId( i );
          if ( selcols && selcolsearch.indexOf( col+"," ) == -1 ) {
            cellValues[i] = "";
            continue;
          }
          var cellid = this.id+"_"+col+"_"+row;
          if ( !cellValues[i] ) {
            cellValues[i] = $( cellid ).innerHTML;
          }
          else if ( cellValues[i] != $( cellid ).innerHTML ) {
            cellValues[i] = "";
          }
        }
      }
      for ( var i=0; i<cellValues.length; i++ ) {
        if ( cellValues[i] ) {
          if ( cellValues[i]=="&nbsp;" ) {
            $( this.cellForms[i] ).value = "";
          }
          else {
            $( this.cellForms[i] ).value = cellValues[i];
          }
        }
      }
    }
    this.addForm.style.display = "table-row";
    $( this.cellForms[0] ).focus();
  }
}

m.table.prototype.addFormCancel = function() {
  if ( this.addForm.status=="clean" ) {
    this.addForm.status = false;
    this.addForm.style.display = "none";
  }
  else if ( this.addForm.status=="custom" ) {
    this.addForm.status = false;
    for( var i=0; i<this.cellForms.length; i++ ) {
      $( this.cellForms[ i ] ).value = null;
    }
    this.addForm.style.display = "none";
  }
  else if ( this.addForm.status=="dirty" ) {
    if ( confirm("Form has changed, really cancel?") ) {
      this.addForm.status = false;
      for( var i=0; i<this.cellForms.length; i++ ) {
        $( this.cellForms[ i ] ).value = null;
      }
      this.addForm.style.display = "none";
    }
  }
  else {
    alert("Form cannot be cancelled.")
  }
}

m.table.prototype.addFormSubmit = function() {
  
}

m.table.prototype.addFormRecall = function() {
  
}

m.table.prototype.addFormCommit = function() {
  
}

m.table.prototype.addFormMessage = function( message ) {
  
}

m.table.prototype.editFormDisplay = function( cellid ) {
  // parse cellid
  var cell = cellid.substr( this.id.length + 1 );
  var col = cell.substr( 0, cell.lastIndexOf("_") );
  var colindex = this.getColumnIndex( col );
  var row = cell.substr( cell.lastIndexOf("_")+1 );
  m.log("Will display",col,"column",colindex,"(cellid",cellid,") edit form",this.cellForms[colindex],"at row",row);
  var input = $(this.cellForms[colindex]).cloneNode( true );
  input.id = this.id + "_editForm" + cell;
  input.value = getNodeAttribute( $(cellid), "value" );
  //m.introspect($(cellid).childNodes);
  var cnlength = $(cellid).childNodes.length;
  for(i=0;i<cnlength;i++) {
    var child = $(cellid).removeChild( $(cellid).childNodes[0] );
    if ( child.nodeType==1 ) {
      $(cellid).original = child;
    }
    log("removed child",i,":",child);
  }
  $(cellid).appendChild( input );
  input.status = "clean";
  connect(input,"onkeydown",this,"editFormKeydown");
  connect(input,"onkeypress",this,"editFormKeypress");
  connect(input,"onfocus",this,"editFormFocus");
  connect(input,"onblur",this,"editFormBlur");
  connect(input,"onchange",this,"editFormChange");
  log("Focus",input.id,"now");
  input.focus();
}

m.table.prototype.editFormCancel = function( cellid ) {
  m.introspect($(cellid).childNodes);
  var input = $(cellid).removeChild( $(cellid).childNodes[0] );
  if ( input.status!="clean" ) {
    if ( !confirm("Cell contents have changed, really cancel?") ) return;
  }
  $(cellid).appendChild( $(cellid).original );
  disconnect(input,"onfocus");
  disconnect(input,"onblur");
  disconnect(input,"onchange");
}

m.table.prototype.editFormSubmit = function( cellid ) {
  var input = $(cellid).removeChild( $(cellid).childNodes[0] );
  if ( input.status=="clean" ) {
    m.log("No change to",cellid,"but submitting anyway");
    $(cellid).appendChild( $(cellid).original );
  }
  else {
    var text = document.createTextNode( m.unescapeHTML( input.value ) );
    m.log("Will submit change to",cellid);
    $(cellid).appendChild( text );
  }
  
  disconnect(input,"onfocus");
  disconnect(input,"onblur");
  disconnect(input,"onchange");
}

m.table.prototype.editFormRecall = function( cellid ) {
  
}

m.table.prototype.editFormCommit = function( cellid ) {
  
}

m.table.prototype.editFormMessage = function( cellid, message ) {
  
}

m.table.prototype.editCellNext = function( cellid, increment ) {
  var cell = cellid.substr( this.id.length + 1 );
  var col = cell.substr( 0, cell.lastIndexOf("_") );
  var colindex = 0;
  var rowindex = 0;
  var usecols = [];
  var userows = [];
  if ( this.selectedColumns.length==0 ) {
    colindex = this.getColumnIndex( col );
    usecols = this.cols;
  }
  else {
    colindex = this.getSelectedColumnIndex( col );
    usecols = this.selectedColumns;
  }
  var row = cell.substr( cell.lastIndexOf("_")+1 );
  if ( this.selectedRows.length==0 ) {
    rowindex = this.getRowIndex( row );
    userows = this.rows;
  }
  else {
    rowindex = this.getSelectedRowIndex( row );
    userows = this.selectedRows;
  }
  // increment colindex
  colindex = colindex + increment;
  if ( colindex==usecols.length || colindex < 0 ) {
    rowindex = rowindex + increment;
    if ( rowindex > userows.length-1 || rowindex < 0 ) {
      return;
    }
    if ( colindex < 0 ) {
      colindex = usecols.length - 1;
    }
    else {
      colindex = 0;
    }
    if ( this.selectedRows.length==0 ) {
      row = this.getRowId( rowindex );
    }
    else {
      row = this.getSelectedRowId( rowindex );
    }
  }
  if ( this.selectedColumns.length==0 ) {
    col = this.getColumnId( colindex );
  }
  else {
    col = this.getSelectedColumnId( colindex );
  }
  var newcellid = this.id+"_"+col+"_"+row;
  this.editFormDisplay( newcellid );
}

m.table.prototype.editRowNext = function( cellid, increment ) {
  var cell = cellid.substr( this.id.length + 1 );
  var col = cell.substr( 0, cell.lastIndexOf("_") );
  var row = cell.substr( cell.lastIndexOf("_")+1 );
  var rowindex = 0;
  var userows = [];
  if ( this.selectedRows.length==0 ) {
    rowindex = this.getRowIndex( row );
    userows = this.rows;
  }
  else {
    rowindex = this.getSelectedRowIndex( row );
    userows = this.selectedRows;
  }
  rowindex = rowindex + increment;
  if ( rowindex > userows.length-1 || rowindex < 0 ) {
    return;
  }
  if ( this.selectedRows.length==0 ) {
    row = this.getRowId( rowindex );
  }
  else {
    row = this.getSelectedRowId( rowindex );
  }
  var newcellid = this.id+"_"+col+"_"+row;
  this.editFormDisplay( newcellid );
}

m.table.prototype.getRowId = function( index ) {
  // strip prefix_rowselect from id
  if ( this.rowSelectors[ index ] ) {
    return $(this.rowSelectors[ index ]).id.substr( this.id.length + 10 );
  }
  return false;
}

m.table.prototype.getSelectedRowId = function( index ) {
  // strip prefix_rowselect from id
  if ( this.selectedRows[ index ] ) {
    return $(this.selectedRows[ index ]).id.substr( this.id.length + 10 );
  }
  return false;
}

m.table.prototype.getRowIndex = function( label ) {
  var rowmatch = -1;
  var match = this.id+"_rowselect"+label;
  for ( var i=0; i<this.rowSelectors.length; i++ ) {
    if ( $(this.rowSelectors[i]).id == match ) {
      rowmatch = i;
      i = this.rowSelectors.length;
    }
  }
  return rowmatch;
}

m.table.prototype.getSelectedRowIndex = function( label ) {
  var rowmatch = -1;
  var match = this.id+"_rowselect"+label;
  for ( var i=0; i<this.selectedRows.length; i++ ) {
    if ( $(this.selectedRows[i]).id == match ) {
      rowmatch = i;
      i = this.selectedRows.length;
    }
  }
  return rowmatch;
}


m.table.prototype.getColumnId = function( index ) {
  // strip prefix_col from id
  if ( this.cols[ index ] ) {
    return $(this.cols[ index ]).id.substr( this.id.length + 4 );
  }
  return false;
}

m.table.prototype.getSelectedColumnId = function( index ) {
  // strip prefix_colselect from id
  //m.log("Looking for selected column",index,this.selectedColumns[ index ],$(this.selectedColumns[ index ]));
  if ( this.selectedColumns[ index ] ) {
    return $(this.selectedColumns[ index ]).id.substr( this.id.length + 10 );
  }
  return false;
}

m.table.prototype.getColumnIndex = function( label ) {
  var colmatch = -1;
  var match = this.id+"_col"+label;
  for ( var i=0; i<this.cols.length; i++ ) {
    if ( $(this.cols[i]).id == match ) {
      colmatch = i;
      i = this.cols.length;
    }
  }
  return colmatch;
}

m.table.prototype.getSelectedColumnIndex = function( label ) {
  var colmatch = -1;
  var match = this.id+"_colselect"+label;
  for ( var i=0; i<this.selectedColumns.length; i++ ) {
    if ( $(this.selectedColumns[i]).id == match ) {
      colmatch = i;
      i = this.selectedColumns.length;
    }
  }
  return colmatch;
}


m.table.prototype.getCellId = function( rowlabel, collabel ) {
  // function( "m_table1_row2", "m_table1_colf" ) returns "m_table1_f_2"
  var rowid = this.id + "_row";
  var colid = this.id + "_col";
  return this.id + "_" + collabel.substr( colid.length ) + "_" + rowlabel.substr( rowid.length );
}


m.table.prototype.tableExport = function( x, y ) {
  var exportRows = [];
  var exportCols = [];
  if ( this.selectedRows.length > 0 ) {
    for ( i=0; i<this.selectedRows.length; i++ ) {
      exportRows.push( this.rows[ this.getRowIndex( this.getSelectedRowId(i) ) ] );
    }
  }
  else {
    exportRows = this.rows;
  }
  if ( this.selectedColumns.length > 0 ) {
    for ( i=0; i<this.selectedColumns.length; i++ ) {
      exportCols.push( this.cols[ this.getColumnIndex( this.getSelectedColumnId(i) ) ] );
    }
  }
  else {
    exportCols = this.cols;
  }  
  log("export rows",exportRows);
  log("export columns",exportCols);
  // build a table!
  var exportTable = TABLE( null, 
    THEAD( null, TR( null, function(){ 
      var ths = [];
      for(i=0;i<exportCols.length;i++) {
        if ( hasElementClass( $(exportCols[i]), "address") ) {
          ths.push( TH( null, scrapeText( $(exportCols[i]) ) ) );
          ths.push( TH( null, "Address2" ) );
          ths.push( TH( null, "City" ) );
          ths.push( TH( null, "State" ) );
          ths.push( TH( null, "Zip" ) );
          ths.push( TH( null, "Country" ) );
        }
        else {
          ths.push( TH( null, scrapeText( $(exportCols[i]) ) ) );
        }
      }
      return ths;
    } ) ), 
    TBODY( null, bind( function(){
      var trs = [];
      for(i=0;i<exportRows.length;i++) {
        trs.push( TR( null, bind( function(){ 
          var tds = [];
          for(ic=0;ic<exportCols.length;ic++) {
            var cellid = this.getCellId( exportRows[i], exportCols[ic] );
            //log("cell id",cellid);
            if ( hasElementClass( $(cellid), "address" ) ) {
              var aobj = m.parseAddress( scrapeText( $( cellid ) ) );
              tds.push( TD( null, aobj.address1 ) );
              tds.push( TD( null, aobj.address2 ) );
              tds.push( TD( null, aobj.city ) );
              tds.push( TD( null, aobj.state ) );
              tds.push( TD( null, aobj.zip ) );
              tds.push( TD( null, aobj.country ) );
            }
            else {
              tds.push( TD( null, scrapeText( $( cellid ) ) ) );
            }
          }
          return tds;
        }, this ) ) );
      }
      return trs;
    }, this )
    )
  );
  //log(exportTable.innerHTML);
  //var exportDiv = DIV( {"id":"m_exportPopup","style":"position: absolute; top: 100px; left: 100px; background-color: white; padding: 3px 4px; border: 1px solid #444;"}, TEXTAREA( {"id":"exportedValues","rows":12,"cols":40}, DIV( null, exportTable ).innerHTML ), P(null, A({"href":"#","onclick":"document.body.removeChild($('exportPopup'))"},"close") ) );
  var exportDiv = DIV( {"id":"m_exportPopup"}, 
    P( null, 
      A({"href":"#","onclick":"m.selectChild($('m_exportPopup'),'table')"},"select"), 
      " | ",
      A({"href":"#","onclick":"document.body.removeChild($('m_exportPopup'))"},"close") 
    ), 
    exportTable 
  );
  document.body.appendChild( exportDiv );
  $("m_exportPopup").style.top = y + "px";
  $("m_exportPopup").style.left = x + "px";
  //$("exportedValues").select();
}


// sort callbacks
m.sorters.generic = function( a, b ) {
  acols = $(a).getElementsByTagName("td");
  bcols = $(b).getElementsByTagName("td");
  //var aa = scrapeText( acols.item( window.sortColumn+1 ) );
  //var bb = scrapeText( bcols.item( window.sortColumn+1 ) );
  var aa = getNodeAttribute( acols.item( window.sortColumn+1 ), "value" );
  var bb = getNodeAttribute( bcols.item( window.sortColumn+1 ), "value" );
  if ( parseFloat(aa) == aa ) {
    aa = parseFloat(aa);
  }
  if ( parseFloat(bb) == bb ) {
    bb = parseFloat(bb);
  }
  //m.log( "Comparing",aa,"and",bb," (column",window.sortColumn,")" );
  if (aa==bb) return 0;
  if (aa<bb) return -1;
  return 1;
}

// replace codes with entities
m.unescapeHTML = function ( s ) {
  return s.replace(/&amp;/g, "&"
            ).replace(/&quot;/g, '"'
            ).replace(/&lt;/g, "<"
            ).replace(/&gt;/g, ">");
}

// log function, outputs arguments to console if m.debug is true
m.log = function() {
  if (!m.debug) return;
  var logcall = "log( ";
  for (var i=0; i<arguments.length; i++) {
    logcall += "arguments["+i+"], ";
  }
  logcall += "null )";
  eval( logcall );
}

// introspect function, outputs object properties and methods to console
m.introspect = function ( obj ) {
  var message = "Introspect "+obj+ ":\n";
  for ( var i in obj ) {
    if ( typeof obj[i] == 'function' ) {
      message += i +" ("+typeof obj[i]+")\n";
    }
    else if ( obj[i] ) {
      message += i +" ("+typeof obj[i]+") => "+obj[i].toString()+"\n";
    }
  }
  log(message);
}


// parse an address into address1, address2, city, state, zip, country
m.parseAddress = function( address ) {
  var aobj = { "address1": "", "address2": "", "city": "", "state": "", "zip": "", "country": "USA" };
  if ( !address || address=="" ) return aobj;
  var lines = address.split("\n");
  //introspect(lines);
  // tokenize last line to see if it's a country (one token)
  var lastline = trim( lines[ lines.length-1 ] );
  if ( lastline!="" ) {
    var lltokens = lastline.split(" ");
    if ( lltokens.length==1 ) {
      aobj.country = trim( lines[ lines.length-1 ] );
      lines.pop();
    }
  }
  // split zip off last line
  lastline = trim( lines.pop() );
  while ( lastline=="" ) {
    lastline = trim( lines.pop() );
  }
  if ( lastline && lastline!="" ) {
    var zipstart = lastline.lastIndexOf(" ");
    if ( zipstart > 0 ) {
      aobj.zip = trim( lastline.substr( zipstart+1 ) );
      var citystate = lastline.substr( 0, zipstart ).split(",");
      if ( citystate.length > 1 ) {
        aobj.state = trim( citystate[1] );
      }
      aobj.city = trim( citystate[0] );
    }
  }
  if ( lines.length > 1 ) {
    aobj.address2 = trim( lines.pop() );
  }
  aobj.address1 = trim( lines.pop() );
  return aobj;
}
//var aobj = m.parseAddress( "380 Broadway Street, 5th Floor\nNew York, NY 10013\n\n");
//introspect( aobj );

// select child
m.selectChild = function( element, tagname ) {
  log("looking for",tagname,"in element",element.id);
  var elems = getElementsByTagAndClassName( tagname, null, element );
  if ( elems.length ) {
    var range = document.createRange();
    range.selectNode( elems[0] );
    var sel = m.deselect();
    sel.addRange( range );
    log("selected",elems[0]);
  }
}

// deselect whatever the current selection is
m.deselect = function() {
  var userSelection;
  if (window.getSelection) {
  	userSelection = window.getSelection();
  }
  else if (document.selection) { // should come last; Opera!
  	userSelection = document.selection.createRange();
  }
  if ( userSelection.removeAllRanges ) {
    userSelection.removeAllRanges();
  }
  else {
    userSelection.collapse();
  }
  return userSelection;
}
