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
11goog.provide('cvox.TraverseTable');
12
13goog.require('cvox.DomPredicates');
14goog.require('cvox.DomUtil');
15goog.require('cvox.SelectionUtil');
16goog.require('cvox.TableUtil');
17goog.require('cvox.TraverseUtil');
18
19
20
21/**
22 * An object that represents an active table cell inside the shadow table.
23 * @constructor
24 */
25function ShadowTableNode() {}
26
27
28/**
29 * Whether or not the active cell is spanned by a preceding cell.
30 * @type {boolean}
31 */
32ShadowTableNode.prototype.spanned;
33
34
35/**
36 * Whether or not this cell is spanned by a rowSpan.
37 * @type {?boolean}
38 */
39ShadowTableNode.prototype.rowSpan;
40
41
42/**
43 * Whether or not this cell is spanned by a colspan
44 * @type {?boolean}
45 */
46ShadowTableNode.prototype.colSpan;
47
48
49/**
50 * The row index of the corresponding active table cell
51 * @type {?number}
52 */
53ShadowTableNode.prototype.i;
54
55
56/**
57 * The column index of the corresponding active table cell
58 * @type {?number}
59 */
60ShadowTableNode.prototype.j;
61
62
63/**
64 * The corresponding <TD> or <TH> node in the active table.
65 * @type {?Node}
66 */
67ShadowTableNode.prototype.activeCell;
68
69
70/**
71 * The cells that are row headers of the corresponding active table cell
72 * @type {!Array}
73 */
74ShadowTableNode.prototype.rowHeaderCells = [];
75
76
77/**
78 * The cells that are column headers of the corresponding active table cell
79 * @type {!Array}
80 */
81ShadowTableNode.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 */
91cvox.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 */
155cvox.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 */
168cvox.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 */
176cvox.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 */
199cvox.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 */
210cvox.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 */
218cvox.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 */
270cvox.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 */
313cvox.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 */
356cvox.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 */
523cvox.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 */
736cvox.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 */
796cvox.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 */
845cvox.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 */
863cvox.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 */
880cvox.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 */
893cvox.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 */
905cvox.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 */
917cvox.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 */
929cvox.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 */
943cvox.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 */
976cvox.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 */
989cvox.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 */
1006cvox.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 */
1023cvox.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 */
1041cvox.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 */
1058cvox.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  */
1108cvox.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 */
1127cvox.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 */
1145cvox.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 */
1163cvox.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 */
1182cvox.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 */
1206cvox.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 */
1230cvox.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 */
1249cvox.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 */
1273cvox.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 */
1293cvox.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 */
1310cvox.TraverseTable.prototype.resetCursor = function() {
1311  this.currentCellCursor = null;
1312};
1313