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;