Example Start

ARIA Enabled Travel Reimbursement Form Example
Date Departed From Arrived At Automobile
Miles
Mileage
Reimbursement
Other Transportation Cost Lodging Meals Miscellaneous Daily
Total
Location Time Location Time Air, Rails, etc.. Car Rental Taxi, Parking, Tolls, etc..
+
Total Travel Expenses
- - - - - - - -
Messages:

Example End

Example Description

Type: Best Practice

Simple example of a grid widget to implement a travel reimbursement form.

Keyboard Support

Based on the keyboard shortcuts defined in the AOL DHTML Style guide for grid

  • Tab (standard mode): Move focus between grid, links and form controls.
  • Tab (actionable mode): Move focus.
  • ENTER or F2 (standard mode): Edit contents of gridcell.
  • ENTER or F2 (Actionable mode): Update the contents of the gridcell.
  • ESC (Actionable mode): Move to standard mode, do not update the contents of the gridcell.
  • Up Arrow: Move focus to gridcell in previous row
  • Down Arrow: Move focus to gridcell in next row
  • Left Arrow: Move focus one gridcell to the left.
  • Right Arrow: Move focus one gridcell to the right.
  • Home: Move focus to first gridcell in the row.
  • End: Move focus to the last gridcell to the row.
  • Page Up: Move focus to the first gridcell in the columm.
  • Page Down: Move focus to the last gridcell in the column.

Example Markup

Browser Compatibility

HTML Source Code


<div role="application">

    <table id="expenses" role="grid" summary="ARIA Enabled Travel Reimbursement Form Example">
        <caption>ARIA Enabled Travel Reimbursement Form Example</caption>
        <thead>
        <tr>
            <th id="date" rowspan="2">Date</th>
            <th id="from" colspan="2">Departed From</th>
            <th id="to" colspan="2">Arrived At</th>
            <th id="miles" rowspan="2">Automobile<br/> Miles</th>
            <th id="mileage" rowspan="2">Mileage <br/>Reimbursement</th>
            <th id="trans" colspan="3">Other Transportation Cost</th>
            <th id="lodging" rowspan="2">Lodging</th>
            <th id="meals" rowspan="2">Meals</th>
            <th id="misc" rowspan="2">Miscellaneous</th>
            <th id="total" rowspan="2">Daily<br/>Total</th>
        </tr>
        <tr>
            <th id="location1">Location</th>
            <th id="time1">Time</th>
            <th id="location2">Location</th>
            <th id="time2">Time</th>
            <th id="trans-other">Air, Rails, etc..</th>
            <th id="trans-rental">Car Rental</th>
            <th id="trans-misc">Taxi, Parking, Tolls, etc..</th>
        </tr>
        </thead>
        <tbody id="data">
        <tr id="totals" role="row">
            <th id="total-expenses-lbl" class="calc" role="gridcell" colspan="6">
               <div id="addRow" role="button" class="addButton" tabindex="0" title="Add new row" aria-pressed="false" aria-disabled="false">+</div>
               Total Travel Expenses
            </td>
            <td id="calc-total-mileage" class="calc" role="gridcell" headers="total-expenses-lbl mileage" aria-label="Total Mileage">-</td>
            <td id="calc-total-trans-other" class="calc" role="gridcell" headers="total-expenses-lbl trans-other" aria-label="Total air or rail transportation">-</td>
            <td id="calc-total-trans-rental" class="calc" role="gridcell" headers="total-expenses-lbl trans-rental" aria-label="Total car rental">-</td>
            <td id="calc-total-trans-misc" class="calc" role="gridcell" headers="total-expenses-lbl trans-misc" aria-label="Total taxi, parking and tolls">-</td>
            <td id="calc-total-lodging" class="calc" role="gridcell" headers="total-expenses-lbl lodging" aria-label="Total lodging">-</td>
            <td id="calc-total-meals" class="calc" role="gridcell" headers="total-expenses-lbl meals" aria-label="Total meals">-</td>
            <td id="calc-total-misc" class="calc" role="gridcell" headers="total-expenses-lbl misc" aria-label="Total miscellaneous">-</td>
            <td id="calc-total-reimbursement" class="reimbursement" role="gridcell" headers="total-expenses-lbl total" aria-label="Total reimbursement" aria-live="polite">-</td>
        </tr>
        <tr>
            <th id="msg">Messages: </th><td headers="msg" colspan="13" id="alert" role="alert">&nbsp;</td>
        </tr>  
        </tbody>
    </table>

    <div id="debug"></div>  
    
    <div id="row_numbers" class="offscreen"></div>  
</div>

CSS Code


table#expenses {
  margin: 0;
  padding: 0;
  border: 1px solid black;
  border-spacing: 0;
}

caption {
  margin: 0;
  padding: 5px;
  border: 2px solid black;
  font-weight: bold;
  font-size: 1.25em;
  color: #027;
  background-color: #eee;
}

table#expenses th {
  margin: 0;
  padding: 5px;
  border: 1px solid black;
  background-color: #eee;
  color: #027;
}
table#expenses th.date {
background-color: #fff;
font-weight: normal;
}

table#expenses td {
  margin: 0;
  padding: 2px;
   background-color: #fff;
  border: 1px solid black;
  border: 1px solid black;
}

.expense, .miles {
  text-align: right;
}
.calc, .reimbursement {
  background-color: #ffe !important;
  font-weight: bold;
  text-align: right;
  color: #027;
}

#msg, #alert {
  border-top: 3px solid black !important;
}

.offscreen {
  position: absolute;
  top: -30em;
  left: -300em;
}

.edt {
  margin: 1px;
  padding: 0;
  width: 100%;
  border: none;
  background-color: white;
  display: none;
}
.data {
  margin: 0;
  padding: 0;
  width: 100%;
}
div.addButton {
   float: left;
   display: inline;
   width: 1.5em;
   text-align: center;
   font-weight: bold;
   color: #800;
   background-color: #eee;
   border-top: 1px solid #777;
   border-left: 1px solid #777;
   border-right: 2px solid #000;
   border-bottom: 2px solid #000;
}
div.pressed {
   background-color: #fff;
   border-top: 2px solid #000;
   border-left: 2px solid #000;
   border-bottom: 1px solid #777;
   border-right: 1px solid #777;
}
div.disabled {
   background-color: #eee;
   color: #444;
   border-top: 1px solid #777;
   border-left: 1px solid #777;
   border-bottom: 2px solid #000;
   border-right: 2px solid #000;
}
.focus {
  background-color: #79e !important;
}

Javascript Source Code


$(document).ready(function () {

  // Create an instance of the travel Calculator. Parameters are the table to use,
  // the per-mile reimbursement, the maximum number of data rows, and the initial
  // number of rows to create.
  var app = new travelCalc('table#expenses', 0.15, 5, 1);

}); // end ready function

function keyCodes () {
  // Define values for keycodes
  this.backspace  = 8;
  this.tab        = 9;
  this.enter      = 13;
  this.esc        = 27;

  this.space      = 32;
  this.pageup     = 33;
  this.pagedown   = 34;
  this.end        = 35;
  this.home       = 36;

  this.left       = 37;
  this.up         = 38;
  this.right      = 39;
  this.down       = 40;

  this.insert     = 45;
  this.del        = 46;

  this.f2         = 113;
}

//
// travelCalc() is a class to implement a simple travel reimbusement calculator
//
// @param (table string) table is the id of the table to operate on
//
// @param (maxRows integer) maxRows is the maximum number of rows an instance of
// travelCalc may have
//
// @param (initNum integer) initNum is the number of rows to add during object instantiation
//
// @return N/A
//
function travelCalc(table, reimbursement, maxRows, initNum) {

  var thisObj = this; // Store the this pointer

  // Define class properties
  this.reimbursement = reimbursement;
  this.maxRows = maxRows; // maximum number of rows allowed this instance
  this.numRows = 0;  // The current number of rows belonging to instance
  this.curRow = 0;  // The currently selected row
  this.curCol = 0;  // The currently selected column
  this.$tbody = $(table).find('tbody#data'); // Store the tbody object
   this.$addButton = $(table).find('.addButton'); // Store the add row button object
  this.editMode = false;  // True if in edit mode

  this.keys = new keyCodes();

  // Bind handlers
  this.bindHandlers();

  // Add rows
  for (var ndx = 0; ndx < initNum; ndx += 1) {
    this.addRow();
  }

  // Store the collection of editable table cells in an object property
  // this property must be updated when adding new rows.
  this.$editableCells = this.$tbody.find('td.editable,th.editable');

  // Make first row navigable
  $('#row1-date').attr('tabindex', '0');

} // end travelCalc constructor

//
// addRow() is a member function to add a row to the data grid. Function builds a string containing
// the elements to add, and appends the string to the table. addRow() will not add a new row if
// maxRows has been reached.
//
// @return N/A
//
travelCalc.prototype.addRow = function() {
  var thisObj = this;  // store a pointer to this object

  // Do not add a new row if maxRows has been reached
  if (this.numRows < this.maxRows) {
    var label;

    // Increment the number of rows
    this.numRows += 1;

    var row = '<tr role="row" id="row' + (this.numRows) + '">';

    // date cell
    label = 'aria-labelledby="date r' + this.numRows + '"';
    row += '<th id="row' + this.numRows + '-date" role="gridcell" ' +
      'class="date editable" ' + label + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-date-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-date-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter travel date of trip ' + this.numRows + '" />' +
      '</th>';

    // Departure location cell
    headers = 'headers="location1 row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-location1" role="gridcell" ' +
      'class="location editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-location1-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-location1-edt" class="edt" type="text" aria-hidden="true" size="12" title="Enter departure location for trip ' + this.numRows + '" />' +
      '</td>';

    // Departure time cell
    headers = 'headers="time1 row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-time1" role="gridcell" ' +
      'class="time editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-time1-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-time1-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter departure time for trip ' + this.numRows + '" />' +
      '</td>';

    // Arrived at cell
    headers = 'headers="location2 row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-location2" role="gridcell" ' +
      'class="location editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-location2-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-location2-edt" class="edt" type="text" aria-hidden="true" size="12" title="Enter destination for trip ' + this.numRows + '" />' +
      '</td>';

    // Arrival time cell
    headers = 'headers="time2 row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-time2" role="gridcell" ' +
      'class="time editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-time2-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-time2-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter arrival time for trip ' + this.numRows + '" />' +
      '</td>';

    // Automobile miles cell
    headers = 'headers="miles row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-miles" role="gridcell" ' +
      'class="miles editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-miles-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-miles-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter miles driven for trip ' + this.numRows + '" />' +
      '</td>';

    // Mileage reimbursement cell
    headers = 'headers="mileage row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-mileage" role="gridcell" ' +
      'class="calc" ' + headers + 'tabindex="-1">-</td>';

    // Air et al. transportation cost cell
    headers = 'headers="trans-other row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-trans-other" role="gridcell" ' +
      'class="expense editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-trans-other-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-trans-other-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter air, rail, or other major travel expense for trip ' +
         this.numRows + '" />' +
      '</td>';

    // Car rental transportation cost cell
    headers = 'headers="trans-rental row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-trans-rental" role="gridcell" ' +
      'class="expense editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-trans-rental-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-trans-rental-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter car rental expense for trip ' + this.numRows + '" />' +
      '</td>';

    // Misc. transportation cost cell
    headers = 'headers="trans-misc row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-trans-misc" role="gridcell" ' +
      'class="expense editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-trans-misc-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-trans-misc-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter miscellaneous expenses, such as parking, tolls, or taxi, for trip ' +
         this.numRows + '" />' +
      '</td>';

    // Lodging cost cell
    headers = 'headers="lodging row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-lodging" role="gridcell" ' +
      'class="expense editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-lodging-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-lodging-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter lodging expense for trip ' + this.numRows + '" />' +
      '</td>';

    // Meals cost cell
    headers = 'headers="meals row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-meals" role="gridcell" ' +
      'class="expense editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-meals-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-meals-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter meal expense for trip ' + this.numRows + '" />' +
      '</td>';

    // Misc. cost cell
    headers = 'headers="misc row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-misc" role="gridcell" ' +
      'class="expense editable" ' + headers + 'aria-selected="false" tabindex="-1">' +
      '<span id="row' + this.numRows + '-misc-dta" class="data" aria-hidden="false"></span>' +
      '<input id="row' + this.numRows + '-misc-edt" class="edt" type="text" aria-hidden="true" size="6" title="Enter other miscellaneous expenses for trip ' + this.numRows + '" />' +
      '</td>';

    // Row total cell and closing tr
    headers = 'headers="total row' + this.numRows + '-date"';
    row += '<td id="row' + this.numRows + '-total" class="calc" ' + headers + '>-</td></tr>'

    // Append the new row to the grid
    $('tbody').find('tr#totals').before(row);

    // Add an entry for this row in the list of rows
    $('#row_numbers').append('<div id="r' + this.numRows + '">Row ' + this.numRows + '</div>');
  }
   else {
      $('td#alert').text('Maximum number of rows (' + this.maxRows + ') reached.');
         this.$addButton.unbind('mousedown mouseup keydown keyup');
         this.$addButton.addClass('disabled');
         this.$addButton.attr('aria-disabled', 'true');
         this.$addButton.removeClass('pressed');
         this.$addButton.attr('aria-pressed', 'false');
   }

} //end addRow()

//
// bindHandlers() is a member function to bind event handlers to the tbody of the data table. This uses event
// delegation to manage events for the children cells. Event delegation is much faster than binding to each cell,
// and it also allows new table rows to be handled.
//
// @return N/A
//
travelCalc.prototype.bindHandlers = function() {
  var thisObj = this;  // store a pointer to this object
  var $tbody = this.$tbody; // store a pointer to the table body property (saves a dereference)
  var $button = this.$addButton; // store a pointer to the add row button property (saves a dereference)

  /************ Bind the handlers for the editable grid cells  in the data table **********/
  //

  // bind a click handler
  $tbody.delegate('.editable', 'click', function (e) {
      return thisObj.handleCellClick(this, e);
  }); // end click handler

  // bind a double click handler
  $tbody.delegate('.editable', 'dblclick', function (e) {
      return thisObj.handleCellDblclick(this, e);
  }); // end double click handler

  // bind a keydown handler
  $tbody.delegate('.editable', 'keydown', function (e) {
      return thisObj.handleCellKeyDown(this, e);
  }); // end keydown handler

  // bind a keypress handler - consume events for Opera
  $tbody.delegate('.editable', 'keypress', function (e) {
      return thisObj.handleCellKeyPress(this, e);
  }); // end keyup handler

  // bind a focus handler
  $tbody.delegate('.editable', 'focus', function (e) {
      return thisObj.handleCellFocus(this, e);
  }); // end focus handler

  // bind a blur handler
  $tbody.delegate('.editable', 'blur', function (e) {
      return thisObj.handleCellBlur(this, e);
  }); // end blur handler


  /************ Bind the handlers for the edit boxes in the editable cells **********/
  
  // bind a keydown handler
  $tbody.delegate('input.edt', 'keydown', function (e) {
      return thisObj.handleEditKeyDown(this, e);
  }); // end edit box keydown handler

  // bind a keypress handler - consume events for Opera
  $tbody.delegate('input.edt', 'keypress', function (e) {
      return thisObj.handleEditKeyPress(this, e);
  }); // end edit box keyup handler

  // bind a focus handler
  $tbody.delegate('input.edt', 'focus', function (e) {
      return thisObj.handleEditFocus(this, e);
  }); // end edit box focus handler

  // bind a blur handler
  $tbody.delegate('input.edt', 'blur', function (e) {
      return thisObj.handleEditBlur(this, e);
  }); // end edit box blurhandler

  /************ Bind the handlers for the add row button **********/
  
  // bind a mousedown handler
  $button.mousedown(function (e) {
      return thisObj.handleAddMouseDown(this, e);
  }); // end add button mousedown handler

  // bind a mouseup handler
  $button.mouseup(function (e) {
      return thisObj.handleAddMouseUp(this, e);
  }); // end add button mouseup handler

  // bind a keydown handler
  $button.keydown(function (e) {
      return thisObj.handleAddKeyDown(this, e);
  }); // end add button keydown handler

  // bind a keyup handler
  $button.keyup(function (e) {
      return thisObj.handleAddKeyUp(this, e);
  }); // end add button keyup handler

  // bind a focus handler
  $button.focus(function (e) {
      return thisObj.handleAddFocus(this, e);
  }); // end add button focus handler

  // bind a blur handler
  $button.blur(function (e) {
      return thisObj.handleAddBlur(this, e);
  }); // end add button blur handler

} // end bindHandlers()


//
// enterEditMode() is a member function to enter the edit mode of an editable cell
//
// @param (id object) id is the jQuery object of the cell to operate on
//
// @return N/A
//
travelCalc.prototype.enterEditMode = function($id) {

  var $edt = $id.find('.edt');
  var $data = $id.find('.data');
  
  // Clear any old alert messages
  $('td#alert').text('');

  // Set the editMode flag to true -- make edit mode modal
  // This is faster than unbinding edit cell event handlers
  this.editMode = true;

  // Copy the data from the cell's data span into the edit box
  $edt.val($data.text());

  // hide the data and show the edit box
  $data.hide();
   $data.attr('aria-hidden', 'true');
  $edt.show();
   $edt.attr('aria-hidden', 'false');

  // give the edit box focus
  $edt.focus();

} // end enterEditMode()

//
// leaveEditMode() is a member function to exit the edit mode of an editable cell
//
// @param (id object) id is the jQuery object of the cell to operate on
//
// @return N/A
//
travelCalc.prototype.leaveEditMode = function(id) {

  var $cell = $(id);
  var $edt = $cell.find('.edt');
  var $data = $cell.find('.data');
  var thisObj = this;
  var validEntry = true;


  // Make sure we are actually in edit mode
  if (this.editMode == false) {
    return;
  }

  // Validate the input
  if ($cell.hasClass('date')) {

    if (this.validateDate($edt) == true) {

      // Store the changes
      $data.text($edt.val());
    }
  }
  else if ($cell.hasClass('time')) {

    if (this.validateTime($edt) == true) {

      // Store the changes
      $data.text($edt.val());
    }
  }
  else if ($cell.hasClass('miles')) {

    if (this.validateMiles($edt) == true) {

      var $reimbursementCell = $cell.next();

      // Store the changes
      $data.text($edt.val());

      // Calculate the mileage reimbursement
      $reimbursementCell.text( this.calcMileAmount($edt.val()) );

      // recalculate the daily total
      this.calcDaily($cell.attr('id').split('-')[0])

      // recalculate the mileage reimbursement total
      this.calcMileageTotal();

      // recalculate the total reimbursement
      this.calcTotalReimbursement();
    }
  }
  else if ($cell.hasClass('expense')) {

    if (this.validateExpense($edt) == true) {

      // Store the changes
      $data.text($edt.val());

      // recalculate the daily total
      this.calcDaily($cell.attr('id').split('-')[0])

      // recalculate the column total
      this.calcExpenseTotal($cell.attr('id'))

      // recalculate the total reimbursement
      this.calcTotalReimbursement();
    }
  }
  else {
    // Don't validate; just store the changes
    $data.text($edt.val());
  }

  // Set the editMode flag to false
  this.editMode = false;

  // Hide the edit box and show the data
  $edt.hide();
   $edt.attr('aria-hidden', 'true');
  $data.show();
   $data.attr('aria-hidden', 'false');

  // Give the parent focus
  $cell.focus();


} // end leaveEditMode()

//
// validateDate() is a member function to validate data entered in the date column
//
// @param (id object) id is the jQuery object of the edit box the user modified
//
// @return true if valid entry; false if invalid
//
travelCalc.prototype.validateDate = function($edt) {

  var curDate = new Date();

  if ($edt.val() != "") {
    // try parsing as date using JavaScript Date constructor
    var dateValue = new Date($edt.val().replace(/-/g, '/'));


    if (isFinite(dateValue)) {

      // If user entered 2-digit year, the date will be approximately 100 years off. Check for this and correct
      if (curDate.getFullYear() - dateValue.getFullYear() >= 99) {
        dateValue.setFullYear(dateValue.getFullYear() + 100);
      }

      // Check if date entered is in the future
      if (dateValue > curDate) {
        $('td#alert').text('Date must be before the current date');
        return false;
      }

      // Set date to 60 days in the past
      curDate.setDate(curDate.getDate() - 60);

      // Check if date entered is older than 60 days ago
      if (dateValue < curDate) {
        $('td#alert').text('Date must be within the last 60 days');
        return false;
      }

      // format as mm/dd/yyyy
      $edt.val( (dateValue.getMonth() + 1) + '/' + dateValue.getDate() + '/' + dateValue.getFullYear() );
      return true;
    }
    else {
      $('td#alert').text('Date needs to be in date format, such as 1/31/2001.');
      return false;
    }
  }

  return false;

} // end validateDate()

//
// validateTime() is a member function to validate data entered in the time columns
//
// @param (id object) id is the jQuery object of the edit box the user modified
//
// @return true if valid entry; false if invalid
//
travelCalc.prototype.validateTime = function($edt) {

  if ($edt.val() != "") {
    var str = $.trim($edt.val());

    if (/^(0?[1-9]|1[0-2]):[0-5]\d ?([ap]m?)?$/.test(str) == false) {

      $('td#alert').text('Time must be in valid time format, such as h:mm [am|pm]');
      return false;
    }
  }

  return true;

} // end validateTime()

//
// validateMiles() is a member function to validate data entered in the Automobile Miles column
//
// @param (id object) id is the jQuery object of the edit box the user modified
//
// @return true if valid entry; false if invalid
//
travelCalc.prototype.validateMiles = function($edt) {

  if ($edt.val() != "") {
    var str = $.trim($edt.val());

    if (/^\d*$/.test(str) == false) {

      $('td#alert').text('Automobile Miles must be a number');
      return "Invalid";
    }
  }

  return true;

} // end validateMiles()

//
// validateExpense() is a member function to validate data entered in the expense columns
//
// @param (id object) id is the jQuery object of the edit box the user modified
//
// @return true if valid entry; false if invalid
//
travelCalc.prototype.validateExpense = function($edt) {

  if ($edt.val() != "") {
    var str = $.trim($edt.val());

    if (/^\$?[1-9]\d{0,2}(,\d{3})*(\.\d{0,2})?$/.test(str) ) {

      if (str.charAt(0) != '$') {
        str = '$' + str;
      }
      
      if (/\.\d$/.test(str)) {
        str += "0";
      }
      else if (/\.$/.test(str) ) {
        str += "00";
      }
      else if (!/\.\d{2}$/.test(str) ) {
        str += ".00";
      }
      $edt.val(str);
      return true;
    }
    else {
      $('td#alert').text('Expense must be in valid US money format, such as $1,000.00');
      return "Invalid";
    }
  }

  return true;

} // end validateExpense()

//
// calcMileAmount() is a member function to calculate the automobile mileage reimbursement for a trip
//
// @param (miles int) miles is the total miles entered in the Automobile Miles column
//
// @return The calculated reimbursement, in U.S. currency format
//
travelCalc.prototype.calcMileAmount = function(miles) {

  var amount = '$' + (miles * this.reimbursement);
  var tmp = amount.split('.');

  // If cents is defined, round to the nearest cent
  if (tmp[1] != undefined) {
    var rounded = Math.round(tmp[1].substr(0,2) + '.' + tmp[1].substr(2));
    amount = tmp[0] + '.' + rounded;
  }

  // Add cents
  if (/\.\d$/.test(amount)) {
    amount += "0";
  }
  else if (/\.$/.test(amount) ) {
    amount += "00";
  }
  else if (!/\.\d{2}$/.test(amount) ) {
    amount += ".00";
  }

  return amount;
}

//
// calcDaily() is a member function to calculate the daily total cost of a trip
//
// @param (row string) row is the id of the current row the user modified
//
// @return N/A
//
travelCalc.prototype.calcDaily = function(row) {
  var total = $('#' + row + '-mileage').text().substr(1); // Strip the '$'

  if (total.length) {
    // remove any ',' from the value and convert to a float
    total = parseFloat(total.replace(/,/g, ''));
  }
  else {
    total = 0;
  }

  // Add the total for each expense
  $('#' + row).find('td.expense').each(function() {
    var expense = $(this).find('.data').text().substr(1);

    if (expense.length) {
      // remove any ',' from the value
      expense = expense.replace(/,/g, '');
        
      total += parseFloat(expense);
    }
  });

  // Add cents
  if (/\.\d$/.test(total)) {
    total += "0";
  }
  else if (/\.$/.test(total) ) {
    total += "00";
  }
  else if (!/\.\d{2}$/.test(total) ) {
    total += ".00";
  }
  
  $('#' + row + '-total').text('$' + total);
}

//
// calcMileageTotal() is a member function to calculate the total mileage Reimbursement amount
//
// @return N/A
//
travelCalc.prototype.calcMileageTotal = function() {

  var total = 0;

  // Iterate through the column, adding the expense to the total.
  for (var row = 1; row <= this.numRows ; row++) {
    var expense = $('#row' + row + '-mileage').text().substr(1); // strip the'$' from the expense

    if (expense.length) {
      // remove any ',' from the value
      expense = expense.replace(/,/g, '');
        
      total += parseFloat(expense);
    }
  }

  // Add cents
  if (/\.\d$/.test(total)) {
    total += "0";
  }
  else if (/\.$/.test(total) ) {
    total += "00";
  }
  else if (!/\.\d{2}$/.test(total) ) {
    total += ".00";
  }

  $('#calc-total-mileage').text('$' + total);
}

//
// calcExpenseTotal() is a member function to calculate the total expense for a column
//
// @param (id string) id is the id of the column to update
//
// @return N/A
//
travelCalc.prototype.calcExpenseTotal = function(id) {

  var total = 0;
  var col = id.substr(id.indexOf('-'));

  // Iterate through the column, adding the expense to the total.
  for (var row = 1; row <= this.numRows ; row++) {
    var expense = $('#row' + row + col).find('span').text().substr(1); // strip the'$' from the expense

    if (expense.length) {
      // remove any ',' from the value
      expense = expense.replace(/,/g, '');
        
      total += parseFloat(expense);
    }
  }

  // Add cents
  if (/\.\d$/.test(total)) {
    total += "0";
  }
  else if (/\.$/.test(total) ) {
    total += "00";
  }
  else if (!/\.\d{2}$/.test(total) ) {
    total += ".00";
  }

  $('#calc-total' + col).text('$' + total);
}

//
// calcTotalReimbursement() is a member function to calculate the total expense reimbursement
//
// @return N/A
//
travelCalc.prototype.calcTotalReimbursement = function() {

  var total = 0;

  // Iterate through the column, adding the expense to the total.
  $('th#total-expenses-lbl').siblings().not('td#calc-total-reimbursement').each(function() {
    var expense = $(this).text().substr(1); // strip the'$' from the expense

    if (expense.length) {
      // remove any ',' from the value
      expense = expense.replace(/,/g, '');
        
      total += parseFloat(expense);
    }
  });

  // Add cents
  if (/\.\d$/.test(total)) {
    total += "0";
  }
  else if (/\.$/.test(total) ) {
    total += "00";
  }
  else if (!/\.\d{2}$/.test(total) ) {
    total += ".00";
  }

  $('#calc-total-reimbursement').text('$' + total);
}

//
// handleCellClick() is a callback for the click event. It is bound to the editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
//
// @return N/A
//
travelCalc.prototype.handleCellClick = function(id, e) {

  $(id).focus();

  e.stopPropagation();
  return false;

} // end handleCellClick()

//
// handleCellDblclick() is a callback for the dblclick event. It is bound to the editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleCellDblclick = function(id, e) {

  // do nothing if we are in editMode
  if (this.editMode == false) {

    // enter the edit mode for the cell
    this.enterEditMode($(id));
  }

  e.stopPropagation();
  return false;

} //end handleCellDblclick()

//
// handleCellKeyDown() is a callback for the keydown event. It is bound to the editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return False if specified key is pressed, true if other keypress
//
travelCalc.prototype.handleCellKeyDown = function(id, e) {

  var $curCell = $(id); // Store the current cell object to prevent repeated DOM traversals

  // do nothing if the shift, alt, or ctrl key is pressed or we are in editMode
  if (e.ctrlKey == true || e.altKey == true || e.shiftKey == true || this.editMode == true) {
    return true;
  }

  switch (e.keyCode) {
    case this.keys.enter:
    case this.keys.f2: {
      // enter the edit mode for the cell
      this.enterEditMode($curCell);

      e.stopPropagation();
      return false;
      break;
    }
    case this.keys.left: {
      var $newCell = $curCell.prev();

      // If there is another editable cell to the right, select it
      if ($newCell.length) {

        if ($newCell.attr('id').search('mileage') > 0) {
          // skip this column
          $newCell = $newCell.prev();
        }

        if ($newCell.hasClass('editable')) {

          // Make the new cell navigable and give it focus
          // Use jQuery chaining for optimization
          $newCell.attr('tabindex', '0').focus();
        }
      }

      e.stopPropagation();
      return false;
      break;
    }
    case this.keys.right: {
      var $newCell = $curCell.next();

      // If there is another editable cell to the right, select it
      if ($newCell.length) {

        if ($newCell.attr('id').search('mileage') > 0) {
          // skip this column
          $newCell = $newCell.next();
        }

        if ($newCell.hasClass('editable')) {

          // Make the new cell navigable and give it focus
          // Use jQuery chaining for optimization
          $newCell.attr('tabindex', '0').focus();
        }
      }

      e.stopPropagation();
      return false;
      break;
    }
    case this.keys.up: {
      // Cell id's are of the form "row#-colName". We need to isolate the row number and
      // column name
      var curRow = $curCell.attr('id');
      var len = curRow.indexOf('-');
      var rowNum = curRow.substr(3, len - 3) - 1;

      if (rowNum > 0)
      {
        // build the id string of the new cell
        var newCell = '#row' + rowNum + '-' + curRow.substr(len + 1);

        // Make the new cell navigable and give it focus
        // Use jQuery chaining for optimization
        $(newCell).attr('tabindex', '0').focus();
      }

      e.stopPropagation();
      return false;
    }
    case this.keys.down: {
      // Cell id's are of the form "row#-colName". We need to isolate the row number and
      // column name
      var curRow = $curCell.attr('id');
      var len = curRow.indexOf('-');
      var rowNum = parseInt(curRow.substr(3, len - 3)) + 1;

      if (rowNum <= this.numRows)
      {
        // build the id string of the new cell
        var newCell = '#row' + rowNum + '-' + curRow.substr(len + 1);

        // Make the new cell navigable and give it focus
        // Use jQuery chaining for optimization
        $(newCell).attr('tabindex', '0').focus();
      }
      e.stopPropagation();
      return false;
    }
    case this.keys.pageup: {
      // Cell id's are of the form "row#-colName". We need to isolate the row number and
      // column name
      var curRow = $curCell.attr('id');
      var len = curRow.indexOf('-');
      var rowNum = parseInt(curRow.substr(3, len - 3)) - 1;

      if (rowNum > 0)
      {
        // build the id string of the new cell
        var newCell = '#row1-' + curRow.substr(len + 1);

        // Make the new cell navigable and give it focus
        // Use jQuery chaining for optimization
        $(newCell).attr('tabindex', '0').focus();
      }

      e.stopPropagation();
      return false;
    }
    case this.keys.pagedown: {
      // Cell id's are of the form "row#-colName". We need to isolate the row number and
      // column name
      var curRow = $curCell.attr('id');
      var len = curRow.indexOf('-');
      var rowNum = parseInt(curRow.substr(3, len - 3)) + 1;

      if (rowNum <= this.numRows)
      {
        // build the id string of the new cell
        var newCell = '#row' + this.numRows + '-' + curRow.substr(len + 1);

        // Make the new cell navigable and give it focus
        // Use jQuery chaining for optimization
        $(newCell).attr('tabindex', '0').focus();
      }
      e.stopPropagation();
      return false;
    }
    case this.keys.home: {
      var row = $curCell.attr('id').split('-')[0];

      // Make the new cell navigable and give it focus
      // Use jQuery chaining for optimization
      $('#' + row + '-date').attr('tabindex', '0').focus();

      e.stopPropagation();
      return false;
      break;
    }
    case this.keys.end: {
      var row = $curCell.attr('id').split('-')[0];

      // Make the new cell navigable and give it focus
      // Use jQuery chaining for optimization
      $('#' + row + '-misc').attr('tabindex', '0').focus();

      e.stopPropagation();
      return false;
      break;
    }

  }

  return true;

} // end handleCellKeyDown

//
// handleCellKeyPress() is a callback for the keypress event. It is bound to the editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return False if specified key is released, true if other key released
//
travelCalc.prototype.handleCellKeyPress = function(id, e) {

  // do nothing if the shift, alt, or ctrl key is pressed or we are in editMode
  if (e.ctrlKey == true || e.altKey == true || e.shiftKey == true || this.editMode == true) {
    return true;
  }
  
  switch (e.keyCode) {
    case this.keys.enter:
    case this.keys.f2:
    case this.keys.left:
    case this.keys.right:
    case this.keys.up:
    case this.keys.down:
    case this.keys.pageup:
    case this.keys.pagedown:
    case this.keys.home:
    case this.keys.end: {

      e.stopPropagation();
      return false;
      break;
    }
  }

  return true;

} // end handleCellKeyPress

//
// handleCellFocus() is a callback for the focus event. It is bound to the editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleCellFocus = function(id, e) {

  // Remove the highlighting from the table cells and remove them from the tab order.
  // Use jQuery chaining for optimization
  this.$editableCells.attr('tabindex', '-1').removeClass('focus').attr('aria-selected', 'false');

  // Add the highlighting for the focused cell and make it navigable
  // Use jQuery Chaining for optimization
  $(id).addClass('focus').attr('tabindex', '0').attr('aria-selected', 'true');

  return true;

} // end handleCellFocus()

//
// handleCellBlur() is a callback for the blur event. It is bound to the editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleCellBlur = function(id, e) {

  var $cell = $(id);
  // do nothing if we are in editMode
  if (this.editMode == false) {
    $cell.removeClass('focus').attr('aria-selected', 'false');
  }
  return true;

} // end handleCellBlur()

//
// handleEditKeyDown() is a callback for the keydown event. It is bound to the edit boxes in the
// editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
//
// @return N/A
//
travelCalc.prototype.handleEditKeyDown = function(id, e) {

  var $parentNode = $(id).parent();
  var $newNode;
  
  // do nothing if the ctrl or alt key is pressed
  if (e.ctrlKey == true || e.altKey == true) {
    return true;
  }

  switch (e.keyCode) {
    case this.keys.tab: {
      var haveNewCell = false;

      if (e.shiftKey) {
        // user pressed shift-tab
        $newNode = $parentNode.prev();
        if ($newNode.length) {
          if ($newNode.attr('id').search('mileage') > 0) {
            $newNode = $newNode.prev();
          }

          haveNewCell = true;
        }
      }
      else {
        $newNode = $parentNode.next();
        if ($newNode.length) {
          if ($newNode.attr('id').search('mileage') > 0) {
            $newNode = $newNode.next();
          }

          haveNewCell = true;
        }
      }
      // leave edit mode
      this.leaveEditMode($parentNode);

      // Select the next editable cell (if possible)
      if (haveNewCell == true && $newNode.is('.editable')) {
        $newNode.focus();
      }

      e.stopPropagation();
      return false;
      break;
    }
    case this.keys.enter:
    case this.keys.f2:
    case this.keys.esc: {
      // leave edit mode
      this.leaveEditMode($parentNode);
      e.stopPropagation();
      return false;
      break;
    }
  }

  return true;

} // end handleEditKeyDown()

//
// handleEditKeyPress() is a callback for the keypress event. It is bound to the edit boxes in the
// editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleEditKeyPress = function(id, e) {

  // do nothing if the ctrl or alt key is pressed
  if (e.ctrlKey == true || e.altKey == true) {
    return true;
  }
  switch (e.keyCode) {
    case this.keys.tab:
    case this.keys.enter:
    case this.keys.f2:
    case this.keys.esc: {
      e.stopPropagation();
      return false;
      break;
    }
  }
  return true;
} // end handleEditKeyPress()

//
// handleEditFocus() is a callback for the focus event. It is bound to the edit boxes in the
// editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleEditFocus = function(id, e) {
  
  var $parentNode = $(id).parent();
  
  return true;
}

//
// handleEditBlur() is a callback for the blur event. It is bound to the edit boxes in the
// editable grid cells
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleEditBlur = function(id, e) {

  var $parentNode = $(id).parent();
  
  // leave edit mode
  this.leaveEditMode($parentNode);

  e.stopPropagation();
  return false;
}

//
// handleAddMouseDown() is a callback for the mousedown event. It is bound to the add row button
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
//
// @return N/A
//
travelCalc.prototype.handleAddMouseDown = function(id, e) {

   $(id).addClass('pressed');
   $(id).attr('aria-pressed', 'true')
   this.addRow();
   $(id).removeClass('pressed');
   $(id).attr('aria-pressed', 'false')
   $(id).focus();

  e.stopPropagation();
  return false;

} // end handleAddMouseDown()

//
// handleAddMouseUp() is a callback for the mouseup event. It is bound to the add row button
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
//
// @return N/A
//
travelCalc.prototype.handleAddMouseUp = function(id, e) {

   $(id).removeClass('pressed');
   $(id).attr('aria-pressed', 'false')

  e.stopPropagation();
  return false;

} // end handleAddMouseUp()

//
// handleAddKeyDown() is a callback for the keydown event. It is bound to the add row button
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleAddKeyDown = function(id, e) {

  // do nothing if the ctrl or alt key is pressed
  if (e.ctrlKey == true || e.altKey == true) {
    return true;
  }
  switch (e.keyCode) {
    case this.keys.enter:
    case this.keys.space: {
         $(id).addClass('pressed');
         $(id).attr('aria-pressed', 'true')
         this.addRow();
      e.stopPropagation();
      return false;
      break;
    }
  }
  return true;
} // end handleAddKeyDown()

//
// handleAddKeyUp() is a callback for the keyup event. It is bound to the add row button
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleAddKeyUp = function(id, e) {

  // do nothing if the ctrl or alt key is pressed
  if (e.ctrlKey == true || e.altKey == true) {
    return true;
  }
  switch (e.keyCode) {
    case this.keys.enter:
    case this.keys.space: {
         $(id).removeClass('pressed');
         $(id).attr('aria-pressed', 'false')
      e.stopPropagation();
      return false;
      break;
    }
  }
  return true;
} // end handleAddKeyDown()

//
// handleAddFocus() is a callback for the focus event. It is bound to the add row button
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleAddFocus = function(id, e) {
  
  $(id).addClass('buttonFocus');
  return true;
}

//
// handleAddBlur() is a callback for the blur event. It is bound to the add row button
//
// @param (id object) id is the element generating the event
//
// @param (e object) e is the event object associated with the event
//
// @return N/A
//
travelCalc.prototype.handleAddBlur = function(id, e) {

  $(id).removeClass('buttonFocus pressed');
   $(id).attr('aria-pressed', 'false')
  return false;
}