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.
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.
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');
22 * An object that represents an active table cell inside the shadow table.
25 function ShadowTableNode() {
27 * The cells that are row headers of the corresponding active table cell
30 this.rowHeaderCells
= [];
33 * The cells that are column headers of the corresponding active table cell
36 this.colHeaderCells
= [];
41 * Whether or not the active cell is spanned by a preceding cell.
44 ShadowTableNode
.prototype.spanned
;
48 * Whether or not this cell is spanned by a rowSpan.
51 ShadowTableNode
.prototype.rowSpan
;
55 * Whether or not this cell is spanned by a colspan
58 ShadowTableNode
.prototype.colSpan
;
62 * The row index of the corresponding active table cell
65 ShadowTableNode
.prototype.i
;
69 * The column index of the corresponding active table cell
72 ShadowTableNode
.prototype.j
;
76 * The corresponding <TD> or <TH> node in the active table.
79 ShadowTableNode
.prototype.activeCell
;
83 * Initializes the traversal with the provided table node.
86 * @param {Node} tableNode The table to be traversed.
88 cvox
.TraverseTable = function(tableNode
) {
91 * The active table <TABLE> node. In this context, "active" means that this is
92 * the table the TraverseTable object is navigating.
96 this.activeTable_
= null;
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.
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).
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.
114 * @type {!Array<Array<ShadowTableNode>>}
117 this.shadowTable_
= [];
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>}
128 this.candidateHeaders_
= [];
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
136 * @type {Array<ShadowTableNode>}
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.
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.
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.
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.
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
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.
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.
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.
215 cvox
.TraverseTable
.prototype.initialize = function(tableNode
) {
219 if (tableNode
== this.activeTable_
) {
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) {
254 if (this.getCell() == null) {
255 this.attachCursorToNearestCell_();
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.
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.
276 while (n
.firstElementChild
&&
277 !(n
.firstElementChild
.tagName
== 'TABLE' ||
278 cvox
.AriaUtil
.isGrid(n
.firstElementChild
))) {
279 n
= n
.firstElementChild
;
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})) {
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
)) {
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).
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!
318 var currentCursor
= this.currentCellCursor
;
320 // Does the current row still exist in the table?
321 var currentRow
= this.shadowTable_
[currentCursor
[0]];
323 // Try last cell of current row
324 this.currentCellCursor
= [currentCursor
[0], (currentRow
.length
- 1)];
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
;
330 // Table has been deleted!
331 this.currentCellCursor
= null;
335 this.shadowTable_
[numRows
- 1][currentCursor
[1]];
337 this.currentCellCursor
= [(numRows
- 1), currentCursor
[1]];
339 // Current column does not exist anymore either.
340 // Move cursor to last cell in table.
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.
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([]);
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
389 if (activeTableCell
.hasAttribute('colspan')) {
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
398 // This is a change from the HTML4 spec:
399 // http://www.w3.org/TR/html401/struct/tables.html#adef-colspan
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
407 if (activeTableCell
.hasAttribute('rowspan')) {
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
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
422 // We will follow Chromium and treat rowspan=0 as equivalent to
425 // Tested in method testRowSpan0 in rowColSpanTable_test.js
427 // Filed as a bug in Chromium: http://crbug.com/58223
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;
440 shadowNode
.j
= shadowTableCol
;
441 shadowNode
.activeCell
= activeTableCell
;
442 shadowNode
.rowHeaderCells
= [];
443 shadowNode
.colHeaderCells
= [];
444 shadowNode
.isRowHeader
= false;
445 shadowNode
.isColHeader
= false;
447 // This position is spanned.
448 shadowNode
.spanned
= true;
449 shadowNode
.rowSpan
= (rowsSpanned
> 1);
450 shadowNode
.colSpan
= (colsSpanned
> 1);
452 shadowNode
.j
= shadowTableCol
;
453 shadowNode
.activeCell
= activeTableCell
;
454 shadowNode
.rowHeaderCells
= [];
455 shadowNode
.colHeaderCells
= [];
456 shadowNode
.isRowHeader
= false;
457 shadowNode
.isColHeader
= false;
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
);
467 // Check and update row group status.
468 if (currentRowParent
== null) {
469 // This is the first row
470 currentRowParent
= allRows
[i
].parentNode
;
473 if (allRows
[i
].parentNode
!= currentRowParent
) {
474 // We're in a different row group now
475 currentRowParent
= allRows
[i
].parentNode
;
476 currentRowGroup
= currentRowGroup
+ 1;
479 shadowNode
.rowGroup
= currentRowGroup
;
481 // Check and update col group status
482 if (colToColGroup
.length
> 0) {
483 shadowNode
.colGroup
= colToColGroup
[shadowTableCol
];
485 shadowNode
.colGroup
= 0;
488 if (! shadowNode
.spanned
) {
489 if (activeTableCell
.id
!= null) {
490 this.idToShadowNode_
[activeTableCell
.id
] = shadowNode
;
494 this.shadowTable_
[i
+ r
][shadowTableCol
+ c
] = shadowNode
;
497 shadowTableCol
+= colsSpanned
;
500 // This position has already been filled (by a previous cell that has
501 // a colspan or a rowspan)
506 return this.shadowTable_
;
511 * Finds header cells from the list of candidate headers and classifies them
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.
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
) {
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
542 if (currentShadowNode
.j
> 0) {
543 if (this.shadowTable_
[currentShadowNode
.i
][currentShadowNode
.j
- 1].
544 activeCell
.tagName
== 'TH') {
545 assumedScope
= 'col';
547 } else if (currentShadowNode
.j
< this.shadowTable_
[currentShadowNode
.i
].
549 if (this.shadowTable_
[currentShadowNode
.i
][currentShadowNode
.j
+ 1].
550 activeCell
.tagName
== 'TH') {
551 assumedScope
= 'col';
554 // This row has a width of 1 cell, just assume this is a colum header
555 assumedScope
= 'col';
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';
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';
571 // This column has a height of 1 cell, just assume that this is
573 assumedScope
= 'row';
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';
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
;
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) {
604 this.shadowTable_
[currentShadowNode
.i
][rightCtr
+ 1];
607 rightShadowNode
.rowHeaderCells
.push(currentCell
);
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
;
621 var downShadowNode
= this.shadowTable_
[downCtr
][currentShadowNode
.j
];
622 if (downShadowNode
== null) {
625 var downCell
= downShadowNode
.activeCell
;
627 if ((downCell
.tagName
== 'TH') ||
628 (downCell
.hasAttribute('scope'))) {
630 if (downCtr
< this.shadowTable_
.length
- 1) {
632 this.shadowTable_
[downCtr
+ 1][currentShadowNode
.j
];
635 downShadowNode
.colHeaderCells
.push(currentCell
);
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
;
648 this.shadowTable_
[currentShadowNode
.i
][cellsInRow
].
649 rowHeaderCells
.push(currentCell
);
652 // Now propagate to rest of row group
653 for (var downCtr
= currentShadowNode
.i
+ 1;
654 downCtr
< this.shadowTable_
.length
;
657 if (this.shadowTable_
[downCtr
][0].rowGroup
!= currentRowGroup
) {
661 for (var rightCtr
= 0;
662 rightCtr
< this.shadowTable_
[downCtr
].length
;
665 this.shadowTable_
[downCtr
][rightCtr
].
666 rowHeaderCells
.push(currentCell
);
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
;
681 if (this.shadowTable_
[currentShadowNode
.i
][cellsInCol
].colGroup
==
683 this.shadowTable_
[currentShadowNode
.i
][cellsInCol
].
684 colHeaderCells
.push(currentCell
);
688 // Now propagate to rest of col group
689 for (var downCtr
= currentShadowNode
.i
+ 1;
690 downCtr
< this.shadowTable_
.length
;
693 for (var rightCtr
= 0;
694 rightCtr
< this.shadowTable_
[downCtr
].length
;
697 if (this.shadowTable_
[downCtr
][rightCtr
].colGroup
==
699 this.shadowTable_
[downCtr
][rightCtr
].
700 colHeaderCells
.push(currentCell
);
704 this.tableColHeaders
.push(currentCell
);
706 if (currentCell
.hasAttribute('headers')) {
707 this.findAttrbHeaders_(currentShadowNode
);
709 if (currentCell
.hasAttribute('aria-describedby')) {
710 this.findAttrbDescribedBy_(currentShadowNode
);
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.
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_
,
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
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
);
764 // This is a column header
765 cvox
.TableUtil
.pushIfNotContained(currentShadowNode
.colHeaderCells
,
766 possibleHeaderNode
.activeCell
);
767 cvox
.TableUtil
.pushIfNotContained(this.tableColHeaders
,
768 possibleHeaderNode
.activeCell
);
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.
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_
,
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
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') ==
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') ==
827 cvox
.TableUtil
.pushIfNotContained(currentShadowNode
.colHeaderCells
,
828 possibleHeaderNode
.activeCell
);
829 cvox
.TableUtil
.pushIfNotContained(this.tableColHeaders
,
830 possibleHeaderNode
.activeCell
);
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.
842 cvox
.TraverseTable
.prototype.getCell = function() {
843 if (!this.currentCellCursor
|| !this.shadowTable_
) {
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.
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
;
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.
877 cvox
.TraverseTable
.prototype.getCellRowHeaders = function() {
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.
890 cvox
.TraverseTable
.prototype.getCellColHeaders = function() {
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.
902 cvox
.TraverseTable
.prototype.isSpanned = function() {
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.
914 cvox
.TraverseTable
.prototype.isRowHeader = function() {
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.
926 cvox
.TraverseTable
.prototype.isColHeader = function() {
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.
940 cvox
.TraverseTable
.prototype.getCol = function() {
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];
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.
956 shadowEntry
.activeCell
);
958 } else if ((shadowEntry
.colSpan
) || (!shadowEntry
.rowSpan
)) {
960 shadowEntry
.activeCell
);
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.
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.
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')) {
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.
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]',
1007 if (captionNodes
.length
> 0) {
1008 return captionNodes
[0].innerHTML
;
1016 * Calculates the number of columns in the shadow table.
1017 * @return {number} The number of columns in the shadow table.
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.
1024 for (var i
= 0; i
< this.shadowTable_
.length
; i
++) {
1025 if (this.shadowTable_
[i
].length
> max
) {
1026 max
= this.shadowTable_
[i
].length
;
1034 * Calculates the number of rows in the table.
1035 * @return {number} The number of rows in the table.
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.
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',
1060 var colNodes
= cvox
.XpathUtil
.evalXPath('child::col', this.activeTable_
);
1062 if ((colgroupNodes
.length
== 0) && (colNodes
.length
== 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
;
1074 for (var i
= 0; i
< colNodes
.length
; i
++) {
1075 if (colNodes
[i
].hasAttribute('span')) {
1076 sum
+= colNodes
[i
].getAttribute('span');
1081 for (i
= 0; i
< colgroupNodes
.length
; i
++) {
1082 var colChildren
= cvox
.XpathUtil
.evalXPath('child::col',
1084 if (colChildren
.length
== 0) {
1085 if (colgroupNodes
[i
].hasAttribute('span')) {
1086 sum
+= colgroupNodes
[i
].getAttribute('span');
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
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);
1110 return this.goToRow(this.currentCellCursor
[0] + 1);
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
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);
1129 return this.goToRow(this.currentCellCursor
[0] - 1);
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
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);
1147 return this.goToCol(this.currentCellCursor
[1] + 1);
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
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);
1165 return this.goToCol(this.currentCellCursor
[1] - 1);
1171 * Moves to the row at the specified index in the table. Updates the cell
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).
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];
1185 this.currentCellCursor
= [index
, this.currentCellCursor
[1]];
1195 * Moves to the column at the specified index in the table. Updates the cell
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).
1203 cvox
.TraverseTable
.prototype.goToCol = function(index
) {
1204 if (index
< 0 || index
>= this.colCount
) {
1207 if (this.currentCellCursor
== null) {
1208 // We haven't started moving through the table yet
1209 this.currentCellCursor
= [0, index
];
1211 this.currentCellCursor
= [this.currentCellCursor
[0], index
];
1218 * Moves to the cell at the specified index <i, j> in the table. Updates the
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).
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]];
1232 this.currentCellCursor
= index
;
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).
1246 cvox
.TraverseTable
.prototype.goToLastCell = function() {
1247 var numRows
= this.shadowTable_
.length
;
1251 var lastRow
= this.shadowTable_
[numRows
- 1];
1252 var lastIndex
= [(numRows
- 1), (lastRow
.length
- 1)];
1254 this.shadowTable_
[lastIndex
[0]][lastIndex
[1]];
1256 this.currentCellCursor
= lastIndex
;
1264 * Moves to the cell at the last index in the current row of the table. Update
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).
1270 cvox
.TraverseTable
.prototype.goToRowLastCell = function() {
1271 var currentRow
= this.currentCellCursor
[0];
1272 var lastIndex
= [currentRow
, (this.shadowTable_
[currentRow
].length
- 1)];
1274 this.shadowTable_
[lastIndex
[0]][lastIndex
[1]];
1276 this.currentCellCursor
= lastIndex
;
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).
1290 cvox
.TraverseTable
.prototype.goToColLastCell = function() {
1291 var currentCol
= this.getCol();
1292 var lastIndex
= [(currentCol
.length
- 1), this.currentCellCursor
[1]];
1294 this.shadowTable_
[lastIndex
[0]][lastIndex
[1]];
1296 this.currentCellCursor
= lastIndex
;
1304 * Resets the table cursors.
1307 cvox
.TraverseTable
.prototype.resetCursor = function() {
1308 this.currentCellCursor
= null;