Home | History | Annotate | Download | only in common
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * TODO(stoarca): This class has become obsolete except for the shadow table.
      7  * Chop most of it away.
      8  * @fileoverview A DOM traversal interface for navigating data in tables.
      9  */
     10 
     11 goog.provide('cvox.TraverseTable');
     12 
     13 goog.require('cvox.DomPredicates');
     14 goog.require('cvox.DomUtil');
     15 goog.require('cvox.SelectionUtil');
     16 goog.require('cvox.TableUtil');
     17 goog.require('cvox.TraverseUtil');
     18 
     19 
     20 
     21 /**
     22  * An object that represents an active table cell inside the shadow table.
     23  * @constructor
     24  */
     25 function ShadowTableNode() {}
     26 
     27 
     28 /**
     29  * Whether or not the active cell is spanned by a preceding cell.
     30  * @type {boolean}
     31  */
     32 ShadowTableNode.prototype.spanned;
     33 
     34 
     35 /**
     36  * Whether or not this cell is spanned by a rowSpan.
     37  * @type {?boolean}
     38  */
     39 ShadowTableNode.prototype.rowSpan;
     40 
     41 
     42 /**
     43  * Whether or not this cell is spanned by a colspan
     44  * @type {?boolean}
     45  */
     46 ShadowTableNode.prototype.colSpan;
     47 
     48 
     49 /**
     50  * The row index of the corresponding active table cell
     51  * @type {?number}
     52  */
     53 ShadowTableNode.prototype.i;
     54 
     55 
     56 /**
     57  * The column index of the corresponding active table cell
     58  * @type {?number}
     59  */
     60 ShadowTableNode.prototype.j;
     61 
     62 
     63 /**
     64  * The corresponding <TD> or <TH> node in the active table.
     65  * @type {?Node}
     66  */
     67 ShadowTableNode.prototype.activeCell;
     68 
     69 
     70 /**
     71  * The cells that are row headers of the corresponding active table cell
     72  * @type {!Array}
     73  */
     74 ShadowTableNode.prototype.rowHeaderCells = [];
     75 
     76 
     77 /**
     78  * The cells that are column headers of the corresponding active table cell
     79  * @type {!Array}
     80  */
     81 ShadowTableNode.prototype.colHeaderCells = [];
     82 
     83 
     84 
     85 /**
     86  * Initializes the traversal with the provided table node.
     87  *
     88  * @constructor
     89  * @param {Node} tableNode The table to be traversed.
     90  */
     91 cvox.TraverseTable = function(tableNode) {
     92 
     93   /**
     94    * The active table <TABLE> node. In this context, "active" means that this is
     95    * the table the TraverseTable object is navigating.
     96    * @type {Node}
     97    * @private
     98    */
     99   this.activeTable_ = null;
    100 
    101   /**
    102    * A 2D array "shadow table" that contains pointers to nodes in the active
    103    * table. More specifically, each cell of the shadow table contains a special
    104    * object ShadowTableNode that has as one of its member variables the
    105    * corresponding cell in the active table.
    106    *
    107    * The shadow table will allow us efficient navigation of tables with
    108    * rowspans and colspans without needing to repeatedly scan the table. For
    109    * example, if someone requests a cell at (1,3), predecessor cells with
    110    * rowspans/colspans mean the cell you eventually return could actually be
    111    * one located at (0,2) that spans out to (1,3).
    112    *
    113    * This shadow table will contain a ShadowTableNode with the (0, 2) index at
    114    * the (1,3) position, eliminating the need to check for predecessor cells
    115    * with rowspan/colspan every time we traverse the table.
    116    *
    117    * @type {!Array.<Array.<ShadowTableNode>>}
    118    * @private
    119    */
    120   this.shadowTable_ = [];
    121 
    122   /**
    123    * An array of shadow table nodes that have been determined to contain header
    124    * cells or information about header cells. This array is collected at
    125    * initialization and then only recalculated if the table changes.
    126    * This array is used by findHeaderCells() to determine table row headers
    127    * and column headers.
    128    * @type {Array.<ShadowTableNode>}
    129    * @private
    130    */
    131   this.candidateHeaders_ = [];
    132 
    133   /**
    134    * An array that associates cell IDs with their corresponding shadow nodes.
    135    * If there are two shadow nodes for the same cell (i.e. when a cell spans
    136    * other cells) then the first one will be associated with the ID. This means
    137    * that shadow nodes that have spanned set to true will not be included in
    138    * this array.
    139    * @type {Array.<ShadowTableNode>}
    140    * @private
    141    */
    142   this.idToShadowNode_ = [];
    143 
    144   this.initialize(tableNode);
    145 };
    146 
    147 
    148 /**
    149  * The cell cursor, represented by an array that stores the row and
    150  * column location [i, j] of the active cell. These numbers are 0-based.
    151  * In this context, "active" means that this is the cell the user is
    152  * currently looking at.
    153  * @type {Array}
    154  */
    155 cvox.TraverseTable.prototype.currentCellCursor;
    156 
    157 
    158 /**
    159  * The number of columns in the active table. This is calculated at
    160  * initialization and then only recalculated if the table changes.
    161  *
    162  * Please Note: We have chosen to use the number of columns in the shadow
    163  * table as the canonical column count. This is important for tables that
    164  * have colspans - the number of columns in the active table will always be
    165  * less than the true number of columns.
    166  * @type {?number}
    167  */
    168 cvox.TraverseTable.prototype.colCount = null;
    169 
    170 
    171 /**
    172  * The number of rows in the active table. This is calculated at
    173  * initialization and then only recalculated if the table changes.
    174  * @type {?number}
    175  */
    176 cvox.TraverseTable.prototype.rowCount = null;
    177 
    178 
    179 /**
    180  * The row headers in the active table. This is calculated at
    181  * initialization and then only recalculated if the table changes.
    182  *
    183  * Please Note:
    184  *  Row headers are defined here as <TH> or <TD> elements. <TD> elements when
    185  *  serving as header cells must have either:
    186  *  - The scope attribute defined
    187  *  - Their IDs referenced in the header content attribute of another <TD> or
    188  *  <TH> element.
    189  *
    190  *  The HTML5 spec specifies that only header <TH> elements can be row headers
    191  *  ( http://dev.w3.org/html5/spec/tabular-data.html#row-header ) but the
    192  *  HTML4 spec says that <TD> elements can act as both
    193  *  ( http://www.w3.org/TR/html401/struct/tables.html#h-11.2.6 ). In the
    194  *  interest of providing meaningful header information for all tables, here
    195  *  we take the position that <TD> elements can act as both.
    196  *
    197  * @type {Array}
    198  */
    199 cvox.TraverseTable.prototype.tableRowHeaders = null;
    200 
    201 
    202 /**
    203  * The column headers in the active table. This is calculated at
    204  * initialization and then only recalculated if the table changes.
    205  *
    206  * Please Note: see comment for tableRowHeaders.
    207  *
    208  * @type {Array}
    209  */
    210 cvox.TraverseTable.prototype.tableColHeaders = null;
    211 
    212 
    213 // TODO (stoarca): tighten up interface to {!Node}
    214 /**
    215  * Initializes the class member variables.
    216  * @param {Node} tableNode The table to be traversed.
    217  */
    218 cvox.TraverseTable.prototype.initialize = function(tableNode) {
    219   if (!tableNode) {
    220     return;
    221   }
    222   if (tableNode == this.activeTable_) {
    223     return;
    224   }
    225   this.activeTable_ = tableNode;
    226   this.currentCellCursor = null;
    227 
    228   this.tableRowHeaders = [];
    229   this.tableColHeaders = [];
    230 
    231   this.buildShadowTable_();
    232 
    233   this.colCount = this.shadowColCount_();
    234   this.rowCount = this.countRows_();
    235 
    236   this.findHeaderCells_();
    237 
    238   // Listen for changes to the active table. If the active table changes,
    239   // rebuild the shadow table.
    240   // TODO (stoarca): Is this safe? When this object goes away, doesn't the
    241   // eventListener stay on the node? Someone with better knowledge of js
    242   // please confirm. If so, this is a leak.
    243   this.activeTable_.addEventListener('DOMSubtreeModified',
    244       goog.bind(function() {
    245         this.buildShadowTable_();
    246         this.colCount = this.shadowColCount_();
    247         this.rowCount = this.countRows_();
    248 
    249         this.tableRowHeaders = [];
    250         this.tableColHeaders = [];
    251         this.findHeaderCells_();
    252 
    253         if (this.colCount == 0 && this.rowCount == 0) {
    254           return;
    255         }
    256 
    257         if (this.getCell() == null) {
    258           this.attachCursorToNearestCell_();
    259         }
    260       }, this), false);
    261 };
    262 
    263 
    264 /**
    265  * Finds the cell cursor containing the specified node within the table.
    266  * Returns null if there is no close cell.
    267  * @param {!Node} node The node for which to find the cursor.
    268  * @return {Array.<number>} The table index for the node.
    269  */
    270 cvox.TraverseTable.prototype.findNearestCursor = function(node) {
    271   // TODO (stoarca): The current structure for representing the
    272   // shadow table is not optimal for this query, but it's not urgent
    273   // since this only gets executed at most once per user action.
    274 
    275   // In case node is in a table but above any individual cell, we go down as
    276   // deep as we can, being careful to avoid going into nested tables.
    277   var n = node;
    278 
    279   while (n.firstElementChild &&
    280          !(n.firstElementChild.tagName == 'TABLE' ||
    281            cvox.AriaUtil.isGrid(n.firstElementChild))) {
    282     n = n.firstElementChild;
    283   }
    284   while (!cvox.DomPredicates.cellPredicate(cvox.DomUtil.getAncestors(n))) {
    285     n = cvox.DomUtil.directedNextLeafNode(n);
    286     // TODO(stoarca): Ugly logic. Captions should be part of tables.
    287     // There have been a bunch of bugs as a result of
    288     // DomUtil.findTableNodeInList excluding captions from tables because
    289     // it makes them non-contiguous.
    290     if (!cvox.DomUtil.getContainingTable(n, {allowCaptions: true})) {
    291       return null;
    292     }
    293   }
    294   for (var i = 0; i < this.rowCount; ++i) {
    295     for (var j = 0; j < this.colCount; ++j) {
    296       if (this.shadowTable_[i][j]) {
    297         if (cvox.DomUtil.isDescendantOfNode(
    298             n, this.shadowTable_[i][j].activeCell)) {
    299           return [i, j];
    300         }
    301       }
    302     }
    303   }
    304   return null;
    305 };
    306 
    307 /**
    308  * Finds the valid cell nearest to the current cell cursor and moves the cell
    309  * cursor there. To be used when the table has changed and the current cell
    310  * cursor is now invalid (doesn't exist anymore).
    311  * @private
    312  */
    313 cvox.TraverseTable.prototype.attachCursorToNearestCell_ = function() {
    314   if (!this.currentCellCursor) {
    315     // We have no idea.  Just go 'somewhere'. Other code paths in this
    316     // function go to the last cell, so let's do that!
    317     this.goToLastCell();
    318     return;
    319   }
    320 
    321   var currentCursor = this.currentCellCursor;
    322 
    323   // Does the current row still exist in the table?
    324   var currentRow = this.shadowTable_[currentCursor[0]];
    325   if (currentRow) {
    326     // Try last cell of current row
    327     this.currentCellCursor = [currentCursor[0], (currentRow.length - 1)];
    328   } else {
    329     // Current row does not exist anymore. Does current column still exist?
    330     // Try last cell of current column
    331     var numRows = this.shadowTable_.length;
    332     if (numRows == 0) {
    333       // Table has been deleted!
    334       this.currentCellCursor = null;
    335       return;
    336     }
    337     var aboveCell =
    338         this.shadowTable_[numRows - 1][currentCursor[1]];
    339     if (aboveCell) {
    340       this.currentCellCursor = [(numRows - 1), currentCursor[1]];
    341     } else {
    342       // Current column does not exist anymore either.
    343       // Move cursor to last cell in table.
    344       this.goToLastCell();
    345     }
    346   }
    347 };
    348 
    349 
    350 /**
    351  * Builds or rebuilds the shadow table by iterating through all of the cells
    352  * ( <TD> or <TH> or role='gridcell' nodes) of the active table.
    353  * @return {!Array} The shadow table.
    354  * @private
    355  */
    356 cvox.TraverseTable.prototype.buildShadowTable_ = function() {
    357   // Clear shadow table
    358   this.shadowTable_ = [];
    359 
    360   // Build shadow table structure. Initialize it as a 2D array.
    361   var allRows = cvox.TableUtil.getChildRows(this.activeTable_);
    362   var currentRowParent = null;
    363   var currentRowGroup = null;
    364 
    365   var colGroups = cvox.TableUtil.getColGroups(this.activeTable_);
    366   var colToColGroup = cvox.TableUtil.determineColGroups(colGroups);
    367 
    368   for (var ctr = 0; ctr < allRows.length; ctr++) {
    369     this.shadowTable_.push([]);
    370   }
    371 
    372   // Iterate through active table by row
    373   for (var i = 0; i < allRows.length; i++) {
    374     var childCells = cvox.TableUtil.getChildCells(allRows[i]);
    375 
    376     // Keep track of position in active table
    377     var activeTableCol = 0;
    378     // Keep track of position in shadow table
    379     var shadowTableCol = 0;
    380 
    381     while (activeTableCol < childCells.length) {
    382 
    383       // Check to make sure we haven't already filled this cell.
    384       if (this.shadowTable_[i][shadowTableCol] == null) {
    385 
    386         var activeTableCell = childCells[activeTableCol];
    387 
    388         // Default value for colspan and rowspan is 1
    389         var colsSpanned = 1;
    390         var rowsSpanned = 1;
    391 
    392         if (activeTableCell.hasAttribute('colspan')) {
    393 
    394           colsSpanned =
    395               parseInt(activeTableCell.getAttribute('colspan'), 10);
    396 
    397           if ((isNaN(colsSpanned)) || (colsSpanned <= 0)) {
    398             // The HTML5 spec defines colspan MUST be greater than 0:
    399             // http://dev.w3.org/html5/spec/Overview.html#attr-tdth-colspan
    400             //
    401             // This is a change from the HTML4 spec:
    402             // http://www.w3.org/TR/html401/struct/tables.html#adef-colspan
    403             //
    404             // We will degrade gracefully by treating a colspan=0 as
    405             // equivalent to a colspan=1.
    406             // Tested in method testColSpan0 in rowColSpanTable_test.js
    407             colsSpanned = 1;
    408           }
    409         }
    410         if (activeTableCell.hasAttribute('rowspan')) {
    411           rowsSpanned =
    412               parseInt(activeTableCell.getAttribute('rowspan'), 10);
    413 
    414           if ((isNaN(rowsSpanned)) || (rowsSpanned <= 0)) {
    415             // The HTML5 spec defines that rowspan can be any non-negative
    416             // integer, including 0:
    417             // http://dev.w3.org/html5/spec/Overview.html#attr-tdth-rowspan
    418             //
    419             // However, Chromium treats rowspan=0 as rowspan=1. This appears
    420             // to be a bug from WebKit:
    421             // https://bugs.webkit.org/show_bug.cgi?id=10300
    422             // Inherited from a bug (since fixed) in KDE:
    423             // http://bugs.kde.org/show_bug.cgi?id=41063
    424             //
    425             // We will follow Chromium and treat rowspan=0 as equivalent to
    426             // rowspan=1.
    427             //
    428             // Tested in method testRowSpan0 in rowColSpanTable_test.js
    429             //
    430             // Filed as a bug in Chromium: http://crbug.com/58223
    431             rowsSpanned = 1;
    432           }
    433         }
    434         for (var r = 0; r < rowsSpanned; r++) {
    435           for (var c = 0; c < colsSpanned; c++) {
    436             var shadowNode = new ShadowTableNode();
    437             if ((r == 0) && (c == 0)) {
    438               // This position is not spanned.
    439               shadowNode.spanned = false;
    440               shadowNode.rowSpan = false;
    441               shadowNode.colSpan = false;
    442               shadowNode.i = i;
    443               shadowNode.j = shadowTableCol;
    444               shadowNode.activeCell = activeTableCell;
    445               shadowNode.rowHeaderCells = [];
    446               shadowNode.colHeaderCells = [];
    447               shadowNode.isRowHeader = false;
    448               shadowNode.isColHeader = false;
    449             } else {
    450               // This position is spanned.
    451               shadowNode.spanned = true;
    452               shadowNode.rowSpan = (rowsSpanned > 1);
    453               shadowNode.colSpan = (colsSpanned > 1);
    454               shadowNode.i = i;
    455               shadowNode.j = shadowTableCol;
    456               shadowNode.activeCell = activeTableCell;
    457               shadowNode.rowHeaderCells = [];
    458               shadowNode.colHeaderCells = [];
    459               shadowNode.isRowHeader = false;
    460               shadowNode.isColHeader = false;
    461             }
    462             // Check this shadowNode to see if it is a candidate header cell
    463             if (cvox.TableUtil.checkIfHeader(shadowNode.activeCell)) {
    464               this.candidateHeaders_.push(shadowNode);
    465             } else if (shadowNode.activeCell.hasAttribute('headers')) {
    466               // This shadowNode has information about other header cells
    467               this.candidateHeaders_.push(shadowNode);
    468             }
    469 
    470             // Check and update row group status.
    471             if (currentRowParent == null) {
    472               // This is the first row
    473               currentRowParent = allRows[i].parentNode;
    474               currentRowGroup = 0;
    475             } else {
    476               if (allRows[i].parentNode != currentRowParent) {
    477                 // We're in a different row group now
    478                 currentRowParent = allRows[i].parentNode;
    479                 currentRowGroup = currentRowGroup + 1;
    480               }
    481             }
    482             shadowNode.rowGroup = currentRowGroup;
    483 
    484             // Check and update col group status
    485             if (colToColGroup.length > 0) {
    486               shadowNode.colGroup = colToColGroup[shadowTableCol];
    487             } else {
    488               shadowNode.colGroup = 0;
    489             }
    490 
    491             if (! shadowNode.spanned) {
    492               if (activeTableCell.id != null) {
    493                 this.idToShadowNode_[activeTableCell.id] = shadowNode;
    494               }
    495             }
    496 
    497             this.shadowTable_[i + r][shadowTableCol + c] = shadowNode;
    498           }
    499         }
    500         shadowTableCol += colsSpanned;
    501         activeTableCol++;
    502       } else {
    503         // This position has already been filled (by a previous cell that has
    504         // a colspan or a rowspan)
    505         shadowTableCol += 1;
    506       }
    507     }
    508   }
    509   return this.shadowTable_;
    510 };
    511 
    512 
    513 /**
    514  * Finds header cells from the list of candidate headers and classifies them
    515  * in two ways:
    516  * -- Identifies them for the entire table by adding them to
    517  * this.tableRowHeaders and this.tableColHeaders.
    518  * -- Identifies them for each shadow table node by adding them to the node's
    519  * rowHeaderCells or colHeaderCells arrays.
    520  *
    521  * @private
    522  */
    523 cvox.TraverseTable.prototype.findHeaderCells_ = function() {
    524   // Forming relationships between data cells and header cells:
    525   // http://dev.w3.org/html5/spec/tabular-data.html
    526   // #header-and-data-cell-semantics
    527 
    528   for (var i = 0; i < this.candidateHeaders_.length; i++) {
    529 
    530     var currentShadowNode = this.candidateHeaders_[i];
    531     var currentCell = currentShadowNode.activeCell;
    532 
    533     var assumedScope = null;
    534     var specifiedScope = null;
    535 
    536     if (currentShadowNode.spanned) {
    537       continue;
    538     }
    539 
    540     if ((currentCell.tagName == 'TH') &&
    541         !(currentCell.hasAttribute('scope'))) {
    542       // No scope specified - compute scope ourselves.
    543       // Go left/right - if there's a header node, then this is a column
    544       // header
    545       if (currentShadowNode.j > 0) {
    546         if (this.shadowTable_[currentShadowNode.i][currentShadowNode.j - 1].
    547             activeCell.tagName == 'TH') {
    548           assumedScope = 'col';
    549         }
    550       } else if (currentShadowNode.j < this.shadowTable_[currentShadowNode.i].
    551           length - 1) {
    552         if (this.shadowTable_[currentShadowNode.i][currentShadowNode.j + 1].
    553             activeCell.tagName == 'TH') {
    554           assumedScope = 'col';
    555         }
    556       } else {
    557         // This row has a width of 1 cell, just assume this is a colum header
    558         assumedScope = 'col';
    559       }
    560 
    561       if (assumedScope == null) {
    562         // Go up/down - if there's a header node, then this is a row header
    563         if (currentShadowNode.i > 0) {
    564           if (this.shadowTable_[currentShadowNode.i - 1][currentShadowNode.j].
    565               activeCell.tagName == 'TH') {
    566             assumedScope = 'row';
    567           }
    568         } else if (currentShadowNode.i < this.shadowTable_.length - 1) {
    569           if (this.shadowTable_[currentShadowNode.i + 1][currentShadowNode.j].
    570               activeCell.tagName == 'TH') {
    571             assumedScope = 'row';
    572           }
    573         } else {
    574           // This column has a height of 1 cell, just assume that this is
    575           // a row header
    576           assumedScope = 'row';
    577         }
    578       }
    579     } else if (currentCell.hasAttribute('scope')) {
    580       specifiedScope = currentCell.getAttribute('scope');
    581     } else if (currentCell.hasAttribute('role') &&
    582         (currentCell.getAttribute('role') == 'rowheader')) {
    583       specifiedScope = 'row';
    584     } else if (currentCell.hasAttribute('role') &&
    585         (currentCell.getAttribute('role') == 'columnheader')) {
    586      specifiedScope = 'col';
    587     }
    588 
    589     if ((specifiedScope == 'row') || (assumedScope == 'row')) {
    590       currentShadowNode.isRowHeader = true;
    591 
    592       // Go right until you hit the edge of the table or a data
    593       // cell after another header cell.
    594       // Add this cell to each shadowNode.rowHeaderCells attribute as you go.
    595       for (var rightCtr = currentShadowNode.j;
    596            rightCtr < this.shadowTable_[currentShadowNode.i].length;
    597            rightCtr++) {
    598 
    599         var rightShadowNode = this.shadowTable_[currentShadowNode.i][rightCtr];
    600         var rightCell = rightShadowNode.activeCell;
    601 
    602         if ((rightCell.tagName == 'TH') ||
    603             (rightCell.hasAttribute('scope'))) {
    604 
    605           if (rightCtr < this.shadowTable_[currentShadowNode.i].length - 1) {
    606             var checkDataCell =
    607                 this.shadowTable_[currentShadowNode.i][rightCtr + 1];
    608           }
    609         }
    610         rightShadowNode.rowHeaderCells.push(currentCell);
    611       }
    612       this.tableRowHeaders.push(currentCell);
    613     } else if ((specifiedScope == 'col') || (assumedScope == 'col')) {
    614       currentShadowNode.isColHeader = true;
    615 
    616       // Go down until you hit the edge of the table or a data cell
    617       // after another header cell.
    618       // Add this cell to each shadowNode.colHeaders attribute as you go.
    619 
    620       for (var downCtr = currentShadowNode.i;
    621            downCtr < this.shadowTable_.length;
    622            downCtr++) {
    623 
    624         var downShadowNode = this.shadowTable_[downCtr][currentShadowNode.j];
    625         if (downShadowNode == null) {
    626           break;
    627         }
    628         var downCell = downShadowNode.activeCell;
    629 
    630         if ((downCell.tagName == 'TH') ||
    631             (downCell.hasAttribute('scope'))) {
    632 
    633           if (downCtr < this.shadowTable_.length - 1) {
    634             var checkDataCell =
    635                 this.shadowTable_[downCtr + 1][currentShadowNode.j];
    636           }
    637         }
    638         downShadowNode.colHeaderCells.push(currentCell);
    639       }
    640       this.tableColHeaders.push(currentCell);
    641     } else if (specifiedScope == 'rowgroup') {
    642        currentShadowNode.isRowHeader = true;
    643 
    644       // This cell is a row header for the rest of the cells in this row group.
    645       var currentRowGroup = currentShadowNode.rowGroup;
    646 
    647       // Get the rest of the cells in this row first
    648       for (var cellsInRow = currentShadowNode.j + 1;
    649            cellsInRow < this.shadowTable_[currentShadowNode.i].length;
    650            cellsInRow++) {
    651         this.shadowTable_[currentShadowNode.i][cellsInRow].
    652             rowHeaderCells.push(currentCell);
    653       }
    654 
    655       // Now propagate to rest of row group
    656       for (var downCtr = currentShadowNode.i + 1;
    657            downCtr < this.shadowTable_.length;
    658            downCtr++) {
    659 
    660         if (this.shadowTable_[downCtr][0].rowGroup != currentRowGroup) {
    661           break;
    662         }
    663 
    664         for (var rightCtr = 0;
    665              rightCtr < this.shadowTable_[downCtr].length;
    666              rightCtr++) {
    667 
    668           this.shadowTable_[downCtr][rightCtr].
    669               rowHeaderCells.push(currentCell);
    670         }
    671       }
    672       this.tableRowHeaders.push(currentCell);
    673 
    674     } else if (specifiedScope == 'colgroup') {
    675       currentShadowNode.isColHeader = true;
    676 
    677       // This cell is a col header for the rest of the cells in this col group.
    678       var currentColGroup = currentShadowNode.colGroup;
    679 
    680       // Get the rest of the cells in this colgroup first
    681       for (var cellsInCol = currentShadowNode.j + 1;
    682            cellsInCol < this.shadowTable_[currentShadowNode.i].length;
    683            cellsInCol++) {
    684         if (this.shadowTable_[currentShadowNode.i][cellsInCol].colGroup ==
    685             currentColGroup) {
    686           this.shadowTable_[currentShadowNode.i][cellsInCol].
    687               colHeaderCells.push(currentCell);
    688         }
    689       }
    690 
    691       // Now propagate to rest of col group
    692       for (var downCtr = currentShadowNode.i + 1;
    693            downCtr < this.shadowTable_.length;
    694            downCtr++) {
    695 
    696         for (var rightCtr = 0;
    697              rightCtr < this.shadowTable_[downCtr].length;
    698              rightCtr++) {
    699 
    700           if (this.shadowTable_[downCtr][rightCtr].colGroup ==
    701               currentColGroup) {
    702             this.shadowTable_[downCtr][rightCtr].
    703                 colHeaderCells.push(currentCell);
    704           }
    705         }
    706       }
    707       this.tableColHeaders.push(currentCell);
    708     }
    709     if (currentCell.hasAttribute('headers')) {
    710       this.findAttrbHeaders_(currentShadowNode);
    711     }
    712     if (currentCell.hasAttribute('aria-describedby')) {
    713       this.findAttrbDescribedBy_(currentShadowNode);
    714     }
    715   }
    716 };
    717 
    718 
    719 /**
    720  * Finds header cells from the 'headers' attribute of a given shadow node's
    721  * active cell and classifies them in two ways:
    722  * -- Identifies them for the entire table by adding them to
    723  * this.tableRowHeaders and this.tableColHeaders.
    724  * -- Identifies them for the shadow table node by adding them to the node's
    725  * rowHeaderCells or colHeaderCells arrays.
    726  * Please note that header cells found through the 'headers' attribute are
    727  * difficult to attribute to being either row or column headers because a
    728  * table cell can declare arbitrary cells as its headers. A guess is made here
    729  * based on which axis the header cell is closest to.
    730  *
    731  * @param {ShadowTableNode} currentShadowNode A shadow node with an active cell
    732  * that has a 'headers' attribute.
    733  *
    734  * @private
    735  */
    736 cvox.TraverseTable.prototype.findAttrbHeaders_ = function(currentShadowNode) {
    737   var activeTableCell = currentShadowNode.activeCell;
    738 
    739   var idList = activeTableCell.getAttribute('headers').split(' ');
    740   for (var idToken = 0; idToken < idList.length; idToken++) {
    741     // Find cell(s) with this ID, add to header list
    742     var idCellArray = cvox.TableUtil.getCellWithID(this.activeTable_,
    743                                                    idList[idToken]);
    744 
    745     for (var idCtr = 0; idCtr < idCellArray.length; idCtr++) {
    746       if (idCellArray[idCtr].id == activeTableCell.id) {
    747         // Skip if the ID is the same as the current cell's ID
    748         break;
    749       }
    750       // Check if this list of candidate headers contains a
    751       // shadowNode with an active cell with this ID already
    752       var possibleHeaderNode =
    753           this.idToShadowNode_[idCellArray[idCtr].id];
    754       if (! cvox.TableUtil.checkIfHeader(possibleHeaderNode.activeCell)) {
    755         // This listed header cell will not be handled later.
    756         // Determine whether this is a row or col header for
    757         // the active table cell
    758 
    759         var iDiff = Math.abs(possibleHeaderNode.i - currentShadowNode.i);
    760         var jDiff = Math.abs(possibleHeaderNode.j - currentShadowNode.j);
    761         if ((iDiff == 0) || (iDiff < jDiff)) {
    762           cvox.TableUtil.pushIfNotContained(currentShadowNode.rowHeaderCells,
    763                                             possibleHeaderNode.activeCell);
    764           cvox.TableUtil.pushIfNotContained(this.tableRowHeaders,
    765                                             possibleHeaderNode.activeCell);
    766         } else {
    767           // This is a column header
    768           cvox.TableUtil.pushIfNotContained(currentShadowNode.colHeaderCells,
    769                                             possibleHeaderNode.activeCell);
    770           cvox.TableUtil.pushIfNotContained(this.tableColHeaders,
    771                                             possibleHeaderNode.activeCell);
    772         }
    773       }
    774     }
    775   }
    776 };
    777 
    778 
    779 /**
    780  * Finds header cells from the 'aria-describedby' attribute of a given shadow
    781  * node's active cell and classifies them in two ways:
    782  * -- Identifies them for the entire table by adding them to
    783  * this.tableRowHeaders and this.tableColHeaders.
    784  * -- Identifies them for the shadow table node by adding them to the node's
    785  * rowHeaderCells or colHeaderCells arrays.
    786  *
    787  * Please note that header cells found through the 'aria-describedby' attribute
    788  * must have the role='rowheader' or role='columnheader' attributes in order to
    789  * be considered header cells.
    790  *
    791  * @param {ShadowTableNode} currentShadowNode A shadow node with an active cell
    792  * that has an 'aria-describedby' attribute.
    793  *
    794  * @private
    795  */
    796 cvox.TraverseTable.prototype.findAttrbDescribedBy_ =
    797     function(currentShadowNode) {
    798   var activeTableCell = currentShadowNode.activeCell;
    799 
    800   var idList = activeTableCell.getAttribute('aria-describedby').split(' ');
    801   for (var idToken = 0; idToken < idList.length; idToken++) {
    802     // Find cell(s) with this ID, add to header list
    803     var idCellArray = cvox.TableUtil.getCellWithID(this.activeTable_,
    804                                                    idList[idToken]);
    805 
    806     for (var idCtr = 0; idCtr < idCellArray.length; idCtr++) {
    807       if (idCellArray[idCtr].id == activeTableCell.id) {
    808         // Skip if the ID is the same as the current cell's ID
    809         break;
    810       }
    811       // Check if this list of candidate headers contains a
    812       // shadowNode with an active cell with this ID already
    813       var possibleHeaderNode =
    814           this.idToShadowNode_[idCellArray[idCtr].id];
    815       if (! cvox.TableUtil.checkIfHeader(possibleHeaderNode.activeCell)) {
    816         // This listed header cell will not be handled later.
    817         // Determine whether this is a row or col header for
    818         // the active table cell
    819 
    820         if (possibleHeaderNode.activeCell.hasAttribute('role') &&
    821             (possibleHeaderNode.activeCell.getAttribute('role') ==
    822                 'rowheader')) {
    823           cvox.TableUtil.pushIfNotContained(currentShadowNode.rowHeaderCells,
    824                                             possibleHeaderNode.activeCell);
    825           cvox.TableUtil.pushIfNotContained(this.tableRowHeaders,
    826                                             possibleHeaderNode.activeCell);
    827         } else if (possibleHeaderNode.activeCell.hasAttribute('role') &&
    828             (possibleHeaderNode.activeCell.getAttribute('role') ==
    829                 'columnheader')) {
    830           cvox.TableUtil.pushIfNotContained(currentShadowNode.colHeaderCells,
    831                                             possibleHeaderNode.activeCell);
    832           cvox.TableUtil.pushIfNotContained(this.tableColHeaders,
    833                                             possibleHeaderNode.activeCell);
    834         }
    835       }
    836     }
    837   }
    838 };
    839 
    840 
    841 /**
    842  * Gets the current cell or null if there is no current cell.
    843  * @return {?Node} The cell <TD> or <TH> or role='gridcell' node.
    844  */
    845 cvox.TraverseTable.prototype.getCell = function() {
    846   if (!this.currentCellCursor || !this.shadowTable_) {
    847     return null;
    848   }
    849 
    850   var shadowEntry =
    851       this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]];
    852 
    853   return shadowEntry && shadowEntry.activeCell;
    854 };
    855 
    856 
    857 /**
    858  * Gets the cell at the specified location.
    859  * @param {Array.<number>} index The index <i, j> of the required cell.
    860  * @return {?Node} The cell <TD> or <TH> or role='gridcell' node at the
    861  * specified location. Null if that cell does not exist.
    862  */
    863 cvox.TraverseTable.prototype.getCellAt = function(index) {
    864   if (((index[0] < this.rowCount) && (index[0] >= 0)) &&
    865       ((index[1] < this.colCount) && (index[1] >= 0))) {
    866     var shadowEntry = this.shadowTable_[index[0]][index[1]];
    867     if (shadowEntry != null) {
    868       return shadowEntry.activeCell;
    869     }
    870   }
    871   return null;
    872 };
    873 
    874 
    875 /**
    876  * Gets the cells that are row headers of the current cell.
    877  * @return {!Array} The cells that are row headers of the current cell. Empty if
    878  * the current cell does not have row headers.
    879  */
    880 cvox.TraverseTable.prototype.getCellRowHeaders = function() {
    881   var shadowEntry =
    882       this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]];
    883 
    884   return shadowEntry.rowHeaderCells;
    885 };
    886 
    887 
    888 /**
    889  * Gets the cells that are col headers of the current cell.
    890  * @return {!Array} The cells that are col headers of the current cell. Empty if
    891  * the current cell does not have col headers.
    892  */
    893 cvox.TraverseTable.prototype.getCellColHeaders = function() {
    894   var shadowEntry =
    895       this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]];
    896 
    897   return shadowEntry.colHeaderCells;
    898 };
    899 
    900 
    901 /**
    902  * Whether or not the current cell is spanned by another cell.
    903  * @return {boolean} Whether or not the current cell is spanned by another cell.
    904  */
    905 cvox.TraverseTable.prototype.isSpanned = function() {
    906   var shadowEntry =
    907       this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]];
    908 
    909   return shadowEntry.spanned;
    910 };
    911 
    912 
    913 /**
    914  * Whether or not the current cell is a row header cell.
    915  * @return {boolean} Whether or not the current cell is a row header cell.
    916  */
    917 cvox.TraverseTable.prototype.isRowHeader = function() {
    918   var shadowEntry =
    919       this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]];
    920 
    921   return shadowEntry.isRowHeader;
    922 };
    923 
    924 
    925 /**
    926  * Whether or not the current cell is a col header cell.
    927  * @return {boolean} Whether or not the current cell is a col header cell.
    928  */
    929 cvox.TraverseTable.prototype.isColHeader = function() {
    930   var shadowEntry =
    931       this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]];
    932 
    933   return shadowEntry.isColHeader;
    934 };
    935 
    936 
    937 /**
    938  * Gets the active column, represented as an array of <TH> or <TD> nodes that
    939  * make up a column. In this context, "active" means that this is the column
    940  * that contains the cell the user is currently looking at.
    941  * @return {Array} An array of <TH> or <TD> or role='gridcell' nodes.
    942  */
    943 cvox.TraverseTable.prototype.getCol = function() {
    944   var colArray = [];
    945   for (var i = 0; i < this.shadowTable_.length; i++) {
    946 
    947     if (this.shadowTable_[i][this.currentCellCursor[1]]) {
    948       var shadowEntry = this.shadowTable_[i][this.currentCellCursor[1]];
    949 
    950       if (shadowEntry.colSpan && shadowEntry.rowSpan) {
    951         // Look at the last element in the column cell aray.
    952         var prev = colArray[colArray.length - 1];
    953         if (prev !=
    954             shadowEntry.activeCell) {
    955           // Watch out for positions spanned by a cell with rowspan and
    956           // colspan. We don't want the same cell showing up multiple times
    957           // in per-column cell lists.
    958           colArray.push(
    959               shadowEntry.activeCell);
    960         }
    961       } else if ((shadowEntry.colSpan) || (!shadowEntry.rowSpan)) {
    962         colArray.push(
    963             shadowEntry.activeCell);
    964       }
    965     }
    966   }
    967   return colArray;
    968 };
    969 
    970 
    971 /**
    972  * Gets the active row <TR> node. In this context, "active" means that this is
    973  * the row that contains the cell the user is currently looking at.
    974  * @return {Node} The active row node.
    975  */
    976 cvox.TraverseTable.prototype.getRow = function() {
    977   var childRows = cvox.TableUtil.getChildRows(this.activeTable_);
    978   return childRows[this.currentCellCursor[0]];
    979 };
    980 
    981 
    982 /**
    983  * Gets the table summary text.
    984  *
    985  * @return {?string} Either:
    986  *     1) The table summary text
    987  *     2) Null if the table does not contain a summary attribute.
    988  */
    989 cvox.TraverseTable.prototype.summaryText = function() {
    990   // see http://code.google.com/p/chromium/issues/detail?id=46567
    991   // for information why this is necessary
    992   if (!this.activeTable_.hasAttribute('summary')) {
    993     return null;
    994   }
    995   return this.activeTable_.getAttribute('summary');
    996 };
    997 
    998 
    999 /**
   1000  * Gets the table caption text.
   1001  *
   1002  * @return {?string} Either:
   1003  *     1) The table caption text
   1004  *     2) Null if the table does not include a caption tag.
   1005  */
   1006 cvox.TraverseTable.prototype.captionText = function() {
   1007   // If there's more than one outer <caption> element, choose the first one.
   1008   var captionNodes = cvox.XpathUtil.evalXPath('caption\[1]',
   1009       this.activeTable_);
   1010   if (captionNodes.length > 0) {
   1011     return captionNodes[0].innerHTML;
   1012   } else {
   1013     return null;
   1014   }
   1015 };
   1016 
   1017 
   1018 /**
   1019  * Calculates the number of columns in the shadow table.
   1020  * @return {number} The number of columns in the shadow table.
   1021  * @private
   1022  */
   1023 cvox.TraverseTable.prototype.shadowColCount_ = function() {
   1024   // As the shadow table is a 2D array, the number of columns is the
   1025   // max number of elements in the second-level arrays.
   1026   var max = 0;
   1027   for (var i = 0; i < this.shadowTable_.length; i++) {
   1028     if (this.shadowTable_[i].length > max) {
   1029       max = this.shadowTable_[i].length;
   1030     }
   1031   }
   1032   return max;
   1033 };
   1034 
   1035 
   1036 /**
   1037  * Calculates the number of rows in the table.
   1038  * @return {number} The number of rows in the table.
   1039  * @private
   1040  */
   1041 cvox.TraverseTable.prototype.countRows_ = function() {
   1042   // Number of rows in a table is equal to the number of TR elements contained
   1043   // by the (outer) TBODY elements.
   1044   var rowCount = cvox.TableUtil.getChildRows(this.activeTable_);
   1045   return rowCount.length;
   1046 };
   1047 
   1048 
   1049 /**
   1050  * Calculates the number of columns in the table.
   1051  * This uses the W3C recommended algorithm for calculating number of
   1052  * columns, but it does not take rowspans or colspans into account. This means
   1053  * that the number of columns calculated here might be lower than the actual
   1054  * number of columns in the table if columns are indicated by colspans.
   1055  * @return {number} The number of columns in the table.
   1056  * @private
   1057  */
   1058 cvox.TraverseTable.prototype.getW3CColCount_ = function() {
   1059   // See http://www.w3.org/TR/html401/struct/tables.html#h-11.2.4.3
   1060 
   1061   var colgroupNodes = cvox.XpathUtil.evalXPath('child::colgroup',
   1062       this.activeTable_);
   1063   var colNodes = cvox.XpathUtil.evalXPath('child::col', this.activeTable_);
   1064 
   1065   if ((colgroupNodes.length == 0) && (colNodes.length == 0)) {
   1066     var maxcols = 0;
   1067     var outerChildren = cvox.TableUtil.getChildRows(this.activeTable_);
   1068     for (var i = 0; i < outerChildren.length; i++) {
   1069       var childrenCount = cvox.TableUtil.getChildCells(outerChildren[i]);
   1070       if (childrenCount.length > maxcols) {
   1071         maxcols = childrenCount.length;
   1072       }
   1073     }
   1074     return maxcols;
   1075   } else {
   1076     var sum = 0;
   1077     for (var i = 0; i < colNodes.length; i++) {
   1078       if (colNodes[i].hasAttribute('span')) {
   1079         sum += colNodes[i].getAttribute('span');
   1080       } else {
   1081         sum += 1;
   1082       }
   1083     }
   1084     for (i = 0; i < colgroupNodes.length; i++) {
   1085       var colChildren = cvox.XpathUtil.evalXPath('child::col',
   1086           colgroupNodes[i]);
   1087       if (colChildren.length == 0) {
   1088         if (colgroupNodes[i].hasAttribute('span')) {
   1089           sum += colgroupNodes[i].getAttribute('span');
   1090         } else {
   1091           sum += 1;
   1092         }
   1093       }
   1094     }
   1095   }
   1096   return sum;
   1097 };
   1098 
   1099 
   1100 /**
   1101  * Moves to the next row in the table. Updates the cell cursor.
   1102  *
   1103  * @return {boolean} Either:
   1104  *    1) True if the update has been made.
   1105  *    2) False if the end of the table has been reached and the update has not
   1106  *       happened.
   1107   */
   1108 cvox.TraverseTable.prototype.nextRow = function() {
   1109   if (!this.currentCellCursor) {
   1110     // We have not started moving through the table yet
   1111     return this.goToRow(0);
   1112   } else {
   1113     return this.goToRow(this.currentCellCursor[0] + 1);
   1114   }
   1115 
   1116 };
   1117 
   1118 
   1119 /**
   1120  * Moves to the previous row in the table. Updates the cell cursor.
   1121  *
   1122  * @return {boolean} Either:
   1123  *    1) True if the update has been made.
   1124  *    2) False if the end of the table has been reached and the update has not
   1125  *       happened.
   1126  */
   1127 cvox.TraverseTable.prototype.prevRow = function() {
   1128   if (!this.currentCellCursor) {
   1129     // We have not started moving through the table yet
   1130     return this.goToRow(this.rowCount - 1);
   1131   } else {
   1132     return this.goToRow(this.currentCellCursor[0] - 1);
   1133   }
   1134 };
   1135 
   1136 
   1137 /**
   1138  * Moves to the next column in the table. Updates the cell cursor.
   1139  *
   1140  * @return {boolean} Either:
   1141  *    1) True if the update has been made.
   1142  *    2) False if the end of the table has been reached and the update has not
   1143  *       happened.
   1144  */
   1145 cvox.TraverseTable.prototype.nextCol = function() {
   1146   if (!this.currentCellCursor) {
   1147     // We have not started moving through the table yet
   1148     return this.goToCol(0);
   1149   } else {
   1150     return this.goToCol(this.currentCellCursor[1] + 1);
   1151   }
   1152 };
   1153 
   1154 
   1155 /**
   1156  * Moves to the previous column in the table. Updates the cell cursor.
   1157  *
   1158  * @return {boolean} Either:
   1159  *    1) True if the update has been made.
   1160  *    2) False if the end of the table has been reached and the update has not
   1161  *       happened.
   1162  */
   1163 cvox.TraverseTable.prototype.prevCol = function() {
   1164   if (!this.currentCellCursor) {
   1165     // We have not started moving through the table yet
   1166     return this.goToCol(this.shadowColCount_() - 1);
   1167   } else {
   1168     return this.goToCol(this.currentCellCursor[1] - 1);
   1169   }
   1170 };
   1171 
   1172 
   1173 /**
   1174  * Moves to the row at the specified index in the table. Updates the cell
   1175  * cursor.
   1176  * @param {number} index The index of the required row.
   1177  * @return {boolean} Either:
   1178  *    1) True if the index is valid and the update has been made.
   1179  *    2) False if the index is not valid (either less than 0 or greater than
   1180  *       the number of rows in the table).
   1181  */
   1182 cvox.TraverseTable.prototype.goToRow = function(index) {
   1183   if (this.shadowTable_[index] != null) {
   1184     if (this.currentCellCursor == null) {
   1185       // We haven't started moving through the table yet
   1186       this.currentCellCursor = [index, 0];
   1187     } else {
   1188       this.currentCellCursor = [index, this.currentCellCursor[1]];
   1189     }
   1190     return true;
   1191   } else {
   1192     return false;
   1193   }
   1194 };
   1195 
   1196 
   1197 /**
   1198  * Moves to the column at the specified index in the table. Updates the cell
   1199  * cursor.
   1200  * @param {number} index The index of the required column.
   1201  * @return {boolean} Either:
   1202  *    1) True if the index is valid and the update has been made.
   1203  *    2) False if the index is not valid (either less than 0 or greater than
   1204  *       the number of rows in the table).
   1205  */
   1206 cvox.TraverseTable.prototype.goToCol = function(index) {
   1207   if (index < 0 || index >= this.colCount) {
   1208     return false;
   1209   }
   1210   if (this.currentCellCursor == null) {
   1211     // We haven't started moving through the table yet
   1212     this.currentCellCursor = [0, index];
   1213   } else {
   1214     this.currentCellCursor = [this.currentCellCursor[0], index];
   1215   }
   1216   return true;
   1217 };
   1218 
   1219 
   1220 /**
   1221  * Moves to the cell at the specified index <i, j> in the table. Updates the
   1222  * cell cursor.
   1223  * @param {Array.<number>} index The index <i, j> of the required cell.
   1224  * @return {boolean} Either:
   1225  *    1) True if the index is valid and the update has been made.
   1226  *    2) False if the index is not valid (either less than 0, greater than
   1227  *       the number of rows or columns in the table, or there is no cell
   1228  *       at that location).
   1229  */
   1230 cvox.TraverseTable.prototype.goToCell = function(index) {
   1231   if (((index[0] < this.rowCount) && (index[0] >= 0)) &&
   1232       ((index[1] < this.colCount) && (index[1] >= 0))) {
   1233     var cell = this.shadowTable_[index[0]][index[1]];
   1234     if (cell != null) {
   1235       this.currentCellCursor = index;
   1236       return true;
   1237     }
   1238   }
   1239   return false;
   1240 };
   1241 
   1242 
   1243 /**
   1244  * Moves to the cell at the last index in the table. Updates the cell cursor.
   1245  * @return {boolean} Either:
   1246  *    1) True if the index is valid and the update has been made.
   1247  *    2) False if the index is not valid (there is no cell at that location).
   1248  */
   1249 cvox.TraverseTable.prototype.goToLastCell = function() {
   1250   var numRows = this.shadowTable_.length;
   1251   if (numRows == 0) {
   1252     return false;
   1253   }
   1254   var lastRow = this.shadowTable_[numRows - 1];
   1255   var lastIndex = [(numRows - 1), (lastRow.length - 1)];
   1256   var cell =
   1257       this.shadowTable_[lastIndex[0]][lastIndex[1]];
   1258   if (cell != null) {
   1259     this.currentCellCursor = lastIndex;
   1260     return true;
   1261   }
   1262   return false;
   1263 };
   1264 
   1265 
   1266 /**
   1267  * Moves to the cell at the last index in the current row  of the table. Update
   1268  * the cell cursor.
   1269  * @return {boolean} Either:
   1270  *    1) True if the index is valid and the update has been made.
   1271  *    2) False if the index is not valid (there is no cell at that location).
   1272  */
   1273 cvox.TraverseTable.prototype.goToRowLastCell = function() {
   1274   var currentRow = this.currentCellCursor[0];
   1275   var lastIndex = [currentRow, (this.shadowTable_[currentRow].length - 1)];
   1276   var cell =
   1277       this.shadowTable_[lastIndex[0]][lastIndex[1]];
   1278   if (cell != null) {
   1279     this.currentCellCursor = lastIndex;
   1280     return true;
   1281   }
   1282   return false;
   1283 };
   1284 
   1285 
   1286 /**
   1287  * Moves to the cell at the last index in the current column  of the table.
   1288  * Update the cell cursor.
   1289  * @return {boolean} Either:
   1290  *    1) True if the index is valid and the update has been made.
   1291  *    2) False if the index is not valid (there is no cell at that location).
   1292  */
   1293 cvox.TraverseTable.prototype.goToColLastCell = function() {
   1294   var currentCol = this.getCol();
   1295   var lastIndex = [(currentCol.length - 1), this.currentCellCursor[1]];
   1296   var cell =
   1297       this.shadowTable_[lastIndex[0]][lastIndex[1]];
   1298   if (cell != null) {
   1299     this.currentCellCursor = lastIndex;
   1300     return true;
   1301   }
   1302   return false;
   1303 };
   1304 
   1305 
   1306 /**
   1307  * Resets the table cursors.
   1308  *
   1309  */
   1310 cvox.TraverseTable.prototype.resetCursor = function() {
   1311   this.currentCellCursor = null;
   1312 };
   1313