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