| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * TODO(stoarca): This class has become obsolete except for the shadow table. |
| * Chop most of it away. |
| * @fileoverview A DOM traversal interface for navigating data in tables. |
| */ |
| |
| goog.provide('cvox.TraverseTable'); |
| |
| goog.require('cvox.DomPredicates'); |
| goog.require('cvox.DomUtil'); |
| goog.require('cvox.SelectionUtil'); |
| goog.require('cvox.TableUtil'); |
| goog.require('cvox.TraverseUtil'); |
| |
| |
| |
| /** |
| * An object that represents an active table cell inside the shadow table. |
| * @constructor |
| */ |
| function ShadowTableNode() {} |
| |
| |
| /** |
| * Whether or not the active cell is spanned by a preceding cell. |
| * @type {boolean} |
| */ |
| ShadowTableNode.prototype.spanned; |
| |
| |
| /** |
| * Whether or not this cell is spanned by a rowSpan. |
| * @type {?boolean} |
| */ |
| ShadowTableNode.prototype.rowSpan; |
| |
| |
| /** |
| * Whether or not this cell is spanned by a colspan |
| * @type {?boolean} |
| */ |
| ShadowTableNode.prototype.colSpan; |
| |
| |
| /** |
| * The row index of the corresponding active table cell |
| * @type {?number} |
| */ |
| ShadowTableNode.prototype.i; |
| |
| |
| /** |
| * The column index of the corresponding active table cell |
| * @type {?number} |
| */ |
| ShadowTableNode.prototype.j; |
| |
| |
| /** |
| * The corresponding <TD> or <TH> node in the active table. |
| * @type {?Node} |
| */ |
| ShadowTableNode.prototype.activeCell; |
| |
| |
| /** |
| * The cells that are row headers of the corresponding active table cell |
| * @type {!Array} |
| */ |
| ShadowTableNode.prototype.rowHeaderCells = []; |
| |
| |
| /** |
| * The cells that are column headers of the corresponding active table cell |
| * @type {!Array} |
| */ |
| ShadowTableNode.prototype.colHeaderCells = []; |
| |
| |
| |
| /** |
| * Initializes the traversal with the provided table node. |
| * |
| * @constructor |
| * @param {Node} tableNode The table to be traversed. |
| */ |
| cvox.TraverseTable = function(tableNode) { |
| |
| /** |
| * The active table <TABLE> node. In this context, "active" means that this is |
| * the table the TraverseTable object is navigating. |
| * @type {Node} |
| * @private |
| */ |
| this.activeTable_ = null; |
| |
| /** |
| * A 2D array "shadow table" that contains pointers to nodes in the active |
| * table. More specifically, each cell of the shadow table contains a special |
| * object ShadowTableNode that has as one of its member variables the |
| * corresponding cell in the active table. |
| * |
| * The shadow table will allow us efficient navigation of tables with |
| * rowspans and colspans without needing to repeatedly scan the table. For |
| * example, if someone requests a cell at (1,3), predecessor cells with |
| * rowspans/colspans mean the cell you eventually return could actually be |
| * one located at (0,2) that spans out to (1,3). |
| * |
| * This shadow table will contain a ShadowTableNode with the (0, 2) index at |
| * the (1,3) position, eliminating the need to check for predecessor cells |
| * with rowspan/colspan every time we traverse the table. |
| * |
| * @type {!Array.<Array.<ShadowTableNode>>} |
| * @private |
| */ |
| this.shadowTable_ = []; |
| |
| /** |
| * An array of shadow table nodes that have been determined to contain header |
| * cells or information about header cells. This array is collected at |
| * initialization and then only recalculated if the table changes. |
| * This array is used by findHeaderCells() to determine table row headers |
| * and column headers. |
| * @type {Array.<ShadowTableNode>} |
| * @private |
| */ |
| this.candidateHeaders_ = []; |
| |
| /** |
| * An array that associates cell IDs with their corresponding shadow nodes. |
| * If there are two shadow nodes for the same cell (i.e. when a cell spans |
| * other cells) then the first one will be associated with the ID. This means |
| * that shadow nodes that have spanned set to true will not be included in |
| * this array. |
| * @type {Array.<ShadowTableNode>} |
| * @private |
| */ |
| this.idToShadowNode_ = []; |
| |
| this.initialize(tableNode); |
| }; |
| |
| |
| /** |
| * The cell cursor, represented by an array that stores the row and |
| * column location [i, j] of the active cell. These numbers are 0-based. |
| * In this context, "active" means that this is the cell the user is |
| * currently looking at. |
| * @type {Array} |
| */ |
| cvox.TraverseTable.prototype.currentCellCursor; |
| |
| |
| /** |
| * The number of columns in the active table. This is calculated at |
| * initialization and then only recalculated if the table changes. |
| * |
| * Please Note: We have chosen to use the number of columns in the shadow |
| * table as the canonical column count. This is important for tables that |
| * have colspans - the number of columns in the active table will always be |
| * less than the true number of columns. |
| * @type {?number} |
| */ |
| cvox.TraverseTable.prototype.colCount = null; |
| |
| |
| /** |
| * The number of rows in the active table. This is calculated at |
| * initialization and then only recalculated if the table changes. |
| * @type {?number} |
| */ |
| cvox.TraverseTable.prototype.rowCount = null; |
| |
| |
| /** |
| * The row headers in the active table. This is calculated at |
| * initialization and then only recalculated if the table changes. |
| * |
| * Please Note: |
| * Row headers are defined here as <TH> or <TD> elements. <TD> elements when |
| * serving as header cells must have either: |
| * - The scope attribute defined |
| * - Their IDs referenced in the header content attribute of another <TD> or |
| * <TH> element. |
| * |
| * The HTML5 spec specifies that only header <TH> elements can be row headers |
| * ( http://dev.w3.org/html5/spec/tabular-data.html#row-header ) but the |
| * HTML4 spec says that <TD> elements can act as both |
| * ( http://www.w3.org/TR/html401/struct/tables.html#h-11.2.6 ). In the |
| * interest of providing meaningful header information for all tables, here |
| * we take the position that <TD> elements can act as both. |
| * |
| * @type {Array} |
| */ |
| cvox.TraverseTable.prototype.tableRowHeaders = null; |
| |
| |
| /** |
| * The column headers in the active table. This is calculated at |
| * initialization and then only recalculated if the table changes. |
| * |
| * Please Note: see comment for tableRowHeaders. |
| * |
| * @type {Array} |
| */ |
| cvox.TraverseTable.prototype.tableColHeaders = null; |
| |
| |
| // TODO (stoarca): tighten up interface to {!Node} |
| /** |
| * Initializes the class member variables. |
| * @param {Node} tableNode The table to be traversed. |
| */ |
| cvox.TraverseTable.prototype.initialize = function(tableNode) { |
| if (!tableNode) { |
| return; |
| } |
| if (tableNode == this.activeTable_) { |
| return; |
| } |
| this.activeTable_ = tableNode; |
| this.currentCellCursor = null; |
| |
| this.tableRowHeaders = []; |
| this.tableColHeaders = []; |
| |
| this.buildShadowTable_(); |
| |
| this.colCount = this.shadowColCount_(); |
| this.rowCount = this.countRows_(); |
| |
| this.findHeaderCells_(); |
| |
| // Listen for changes to the active table. If the active table changes, |
| // rebuild the shadow table. |
| // TODO (stoarca): Is this safe? When this object goes away, doesn't the |
| // eventListener stay on the node? Someone with better knowledge of js |
| // please confirm. If so, this is a leak. |
| this.activeTable_.addEventListener('DOMSubtreeModified', |
| goog.bind(function() { |
| this.buildShadowTable_(); |
| this.colCount = this.shadowColCount_(); |
| this.rowCount = this.countRows_(); |
| |
| this.tableRowHeaders = []; |
| this.tableColHeaders = []; |
| this.findHeaderCells_(); |
| |
| if (this.colCount == 0 && this.rowCount == 0) { |
| return; |
| } |
| |
| if (this.getCell() == null) { |
| this.attachCursorToNearestCell_(); |
| } |
| }, this), false); |
| }; |
| |
| |
| /** |
| * Finds the cell cursor containing the specified node within the table. |
| * Returns null if there is no close cell. |
| * @param {!Node} node The node for which to find the cursor. |
| * @return {Array.<number>} The table index for the node. |
| */ |
| cvox.TraverseTable.prototype.findNearestCursor = function(node) { |
| // TODO (stoarca): The current structure for representing the |
| // shadow table is not optimal for this query, but it's not urgent |
| // since this only gets executed at most once per user action. |
| |
| // In case node is in a table but above any individual cell, we go down as |
| // deep as we can, being careful to avoid going into nested tables. |
| var n = node; |
| |
| while (n.firstElementChild && |
| !(n.firstElementChild.tagName == 'TABLE' || |
| cvox.AriaUtil.isGrid(n.firstElementChild))) { |
| n = n.firstElementChild; |
| } |
| while (!cvox.DomPredicates.cellPredicate(cvox.DomUtil.getAncestors(n))) { |
| n = cvox.DomUtil.directedNextLeafNode(n); |
| // TODO(stoarca): Ugly logic. Captions should be part of tables. |
| // There have been a bunch of bugs as a result of |
| // DomUtil.findTableNodeInList excluding captions from tables because |
| // it makes them non-contiguous. |
| if (!cvox.DomUtil.getContainingTable(n, {allowCaptions: true})) { |
| return null; |
| } |
| } |
| for (var i = 0; i < this.rowCount; ++i) { |
| for (var j = 0; j < this.colCount; ++j) { |
| if (this.shadowTable_[i][j]) { |
| if (cvox.DomUtil.isDescendantOfNode( |
| n, this.shadowTable_[i][j].activeCell)) { |
| return [i, j]; |
| } |
| } |
| } |
| } |
| return null; |
| }; |
| |
| /** |
| * Finds the valid cell nearest to the current cell cursor and moves the cell |
| * cursor there. To be used when the table has changed and the current cell |
| * cursor is now invalid (doesn't exist anymore). |
| * @private |
| */ |
| cvox.TraverseTable.prototype.attachCursorToNearestCell_ = function() { |
| if (!this.currentCellCursor) { |
| // We have no idea. Just go 'somewhere'. Other code paths in this |
| // function go to the last cell, so let's do that! |
| this.goToLastCell(); |
| return; |
| } |
| |
| var currentCursor = this.currentCellCursor; |
| |
| // Does the current row still exist in the table? |
| var currentRow = this.shadowTable_[currentCursor[0]]; |
| if (currentRow) { |
| // Try last cell of current row |
| this.currentCellCursor = [currentCursor[0], (currentRow.length - 1)]; |
| } else { |
| // Current row does not exist anymore. Does current column still exist? |
| // Try last cell of current column |
| var numRows = this.shadowTable_.length; |
| if (numRows == 0) { |
| // Table has been deleted! |
| this.currentCellCursor = null; |
| return; |
| } |
| var aboveCell = |
| this.shadowTable_[numRows - 1][currentCursor[1]]; |
| if (aboveCell) { |
| this.currentCellCursor = [(numRows - 1), currentCursor[1]]; |
| } else { |
| // Current column does not exist anymore either. |
| // Move cursor to last cell in table. |
| this.goToLastCell(); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Builds or rebuilds the shadow table by iterating through all of the cells |
| * ( <TD> or <TH> or role='gridcell' nodes) of the active table. |
| * @return {!Array} The shadow table. |
| * @private |
| */ |
| cvox.TraverseTable.prototype.buildShadowTable_ = function() { |
| // Clear shadow table |
| this.shadowTable_ = []; |
| |
| // Build shadow table structure. Initialize it as a 2D array. |
| var allRows = cvox.TableUtil.getChildRows(this.activeTable_); |
| var currentRowParent = null; |
| var currentRowGroup = null; |
| |
| var colGroups = cvox.TableUtil.getColGroups(this.activeTable_); |
| var colToColGroup = cvox.TableUtil.determineColGroups(colGroups); |
| |
| for (var ctr = 0; ctr < allRows.length; ctr++) { |
| this.shadowTable_.push([]); |
| } |
| |
| // Iterate through active table by row |
| for (var i = 0; i < allRows.length; i++) { |
| var childCells = cvox.TableUtil.getChildCells(allRows[i]); |
| |
| // Keep track of position in active table |
| var activeTableCol = 0; |
| // Keep track of position in shadow table |
| var shadowTableCol = 0; |
| |
| while (activeTableCol < childCells.length) { |
| |
| // Check to make sure we haven't already filled this cell. |
| if (this.shadowTable_[i][shadowTableCol] == null) { |
| |
| var activeTableCell = childCells[activeTableCol]; |
| |
| // Default value for colspan and rowspan is 1 |
| var colsSpanned = 1; |
| var rowsSpanned = 1; |
| |
| if (activeTableCell.hasAttribute('colspan')) { |
| |
| colsSpanned = |
| parseInt(activeTableCell.getAttribute('colspan'), 10); |
| |
| if ((isNaN(colsSpanned)) || (colsSpanned <= 0)) { |
| // The HTML5 spec defines colspan MUST be greater than 0: |
| // http://dev.w3.org/html5/spec/Overview.html#attr-tdth-colspan |
| // |
| // This is a change from the HTML4 spec: |
| // http://www.w3.org/TR/html401/struct/tables.html#adef-colspan |
| // |
| // We will degrade gracefully by treating a colspan=0 as |
| // equivalent to a colspan=1. |
| // Tested in method testColSpan0 in rowColSpanTable_test.js |
| colsSpanned = 1; |
| } |
| } |
| if (activeTableCell.hasAttribute('rowspan')) { |
| rowsSpanned = |
| parseInt(activeTableCell.getAttribute('rowspan'), 10); |
| |
| if ((isNaN(rowsSpanned)) || (rowsSpanned <= 0)) { |
| // The HTML5 spec defines that rowspan can be any non-negative |
| // integer, including 0: |
| // http://dev.w3.org/html5/spec/Overview.html#attr-tdth-rowspan |
| // |
| // However, Chromium treats rowspan=0 as rowspan=1. This appears |
| // to be a bug from WebKit: |
| // https://bugs.webkit.org/show_bug.cgi?id=10300 |
| // Inherited from a bug (since fixed) in KDE: |
| // http://bugs.kde.org/show_bug.cgi?id=41063 |
| // |
| // We will follow Chromium and treat rowspan=0 as equivalent to |
| // rowspan=1. |
| // |
| // Tested in method testRowSpan0 in rowColSpanTable_test.js |
| // |
| // Filed as a bug in Chromium: http://crbug.com/58223 |
| rowsSpanned = 1; |
| } |
| } |
| for (var r = 0; r < rowsSpanned; r++) { |
| for (var c = 0; c < colsSpanned; c++) { |
| var shadowNode = new ShadowTableNode(); |
| if ((r == 0) && (c == 0)) { |
| // This position is not spanned. |
| shadowNode.spanned = false; |
| shadowNode.rowSpan = false; |
| shadowNode.colSpan = false; |
| shadowNode.i = i; |
| shadowNode.j = shadowTableCol; |
| shadowNode.activeCell = activeTableCell; |
| shadowNode.rowHeaderCells = []; |
| shadowNode.colHeaderCells = []; |
| shadowNode.isRowHeader = false; |
| shadowNode.isColHeader = false; |
| } else { |
| // This position is spanned. |
| shadowNode.spanned = true; |
| shadowNode.rowSpan = (rowsSpanned > 1); |
| shadowNode.colSpan = (colsSpanned > 1); |
| shadowNode.i = i; |
| shadowNode.j = shadowTableCol; |
| shadowNode.activeCell = activeTableCell; |
| shadowNode.rowHeaderCells = []; |
| shadowNode.colHeaderCells = []; |
| shadowNode.isRowHeader = false; |
| shadowNode.isColHeader = false; |
| } |
| // Check this shadowNode to see if it is a candidate header cell |
| if (cvox.TableUtil.checkIfHeader(shadowNode.activeCell)) { |
| this.candidateHeaders_.push(shadowNode); |
| } else if (shadowNode.activeCell.hasAttribute('headers')) { |
| // This shadowNode has information about other header cells |
| this.candidateHeaders_.push(shadowNode); |
| } |
| |
| // Check and update row group status. |
| if (currentRowParent == null) { |
| // This is the first row |
| currentRowParent = allRows[i].parentNode; |
| currentRowGroup = 0; |
| } else { |
| if (allRows[i].parentNode != currentRowParent) { |
| // We're in a different row group now |
| currentRowParent = allRows[i].parentNode; |
| currentRowGroup = currentRowGroup + 1; |
| } |
| } |
| shadowNode.rowGroup = currentRowGroup; |
| |
| // Check and update col group status |
| if (colToColGroup.length > 0) { |
| shadowNode.colGroup = colToColGroup[shadowTableCol]; |
| } else { |
| shadowNode.colGroup = 0; |
| } |
| |
| if (! shadowNode.spanned) { |
| if (activeTableCell.id != null) { |
| this.idToShadowNode_[activeTableCell.id] = shadowNode; |
| } |
| } |
| |
| this.shadowTable_[i + r][shadowTableCol + c] = shadowNode; |
| } |
| } |
| shadowTableCol += colsSpanned; |
| activeTableCol++; |
| } else { |
| // This position has already been filled (by a previous cell that has |
| // a colspan or a rowspan) |
| shadowTableCol += 1; |
| } |
| } |
| } |
| return this.shadowTable_; |
| }; |
| |
| |
| /** |
| * Finds header cells from the list of candidate headers and classifies them |
| * in two ways: |
| * -- Identifies them for the entire table by adding them to |
| * this.tableRowHeaders and this.tableColHeaders. |
| * -- Identifies them for each shadow table node by adding them to the node's |
| * rowHeaderCells or colHeaderCells arrays. |
| * |
| * @private |
| */ |
| cvox.TraverseTable.prototype.findHeaderCells_ = function() { |
| // Forming relationships between data cells and header cells: |
| // http://dev.w3.org/html5/spec/tabular-data.html |
| // #header-and-data-cell-semantics |
| |
| for (var i = 0; i < this.candidateHeaders_.length; i++) { |
| |
| var currentShadowNode = this.candidateHeaders_[i]; |
| var currentCell = currentShadowNode.activeCell; |
| |
| var assumedScope = null; |
| var specifiedScope = null; |
| |
| if (currentShadowNode.spanned) { |
| continue; |
| } |
| |
| if ((currentCell.tagName == 'TH') && |
| !(currentCell.hasAttribute('scope'))) { |
| // No scope specified - compute scope ourselves. |
| // Go left/right - if there's a header node, then this is a column |
| // header |
| if (currentShadowNode.j > 0) { |
| if (this.shadowTable_[currentShadowNode.i][currentShadowNode.j - 1]. |
| activeCell.tagName == 'TH') { |
| assumedScope = 'col'; |
| } |
| } else if (currentShadowNode.j < this.shadowTable_[currentShadowNode.i]. |
| length - 1) { |
| if (this.shadowTable_[currentShadowNode.i][currentShadowNode.j + 1]. |
| activeCell.tagName == 'TH') { |
| assumedScope = 'col'; |
| } |
| } else { |
| // This row has a width of 1 cell, just assume this is a colum header |
| assumedScope = 'col'; |
| } |
| |
| if (assumedScope == null) { |
| // Go up/down - if there's a header node, then this is a row header |
| if (currentShadowNode.i > 0) { |
| if (this.shadowTable_[currentShadowNode.i - 1][currentShadowNode.j]. |
| activeCell.tagName == 'TH') { |
| assumedScope = 'row'; |
| } |
| } else if (currentShadowNode.i < this.shadowTable_.length - 1) { |
| if (this.shadowTable_[currentShadowNode.i + 1][currentShadowNode.j]. |
| activeCell.tagName == 'TH') { |
| assumedScope = 'row'; |
| } |
| } else { |
| // This column has a height of 1 cell, just assume that this is |
| // a row header |
| assumedScope = 'row'; |
| } |
| } |
| } else if (currentCell.hasAttribute('scope')) { |
| specifiedScope = currentCell.getAttribute('scope'); |
| } else if (currentCell.hasAttribute('role') && |
| (currentCell.getAttribute('role') == 'rowheader')) { |
| specifiedScope = 'row'; |
| } else if (currentCell.hasAttribute('role') && |
| (currentCell.getAttribute('role') == 'columnheader')) { |
| specifiedScope = 'col'; |
| } |
| |
| if ((specifiedScope == 'row') || (assumedScope == 'row')) { |
| currentShadowNode.isRowHeader = true; |
| |
| // Go right until you hit the edge of the table or a data |
| // cell after another header cell. |
| // Add this cell to each shadowNode.rowHeaderCells attribute as you go. |
| for (var rightCtr = currentShadowNode.j; |
| rightCtr < this.shadowTable_[currentShadowNode.i].length; |
| rightCtr++) { |
| |
| var rightShadowNode = this.shadowTable_[currentShadowNode.i][rightCtr]; |
| var rightCell = rightShadowNode.activeCell; |
| |
| if ((rightCell.tagName == 'TH') || |
| (rightCell.hasAttribute('scope'))) { |
| |
| if (rightCtr < this.shadowTable_[currentShadowNode.i].length - 1) { |
| var checkDataCell = |
| this.shadowTable_[currentShadowNode.i][rightCtr + 1]; |
| } |
| } |
| rightShadowNode.rowHeaderCells.push(currentCell); |
| } |
| this.tableRowHeaders.push(currentCell); |
| } else if ((specifiedScope == 'col') || (assumedScope == 'col')) { |
| currentShadowNode.isColHeader = true; |
| |
| // Go down until you hit the edge of the table or a data cell |
| // after another header cell. |
| // Add this cell to each shadowNode.colHeaders attribute as you go. |
| |
| for (var downCtr = currentShadowNode.i; |
| downCtr < this.shadowTable_.length; |
| downCtr++) { |
| |
| var downShadowNode = this.shadowTable_[downCtr][currentShadowNode.j]; |
| if (downShadowNode == null) { |
| break; |
| } |
| var downCell = downShadowNode.activeCell; |
| |
| if ((downCell.tagName == 'TH') || |
| (downCell.hasAttribute('scope'))) { |
| |
| if (downCtr < this.shadowTable_.length - 1) { |
| var checkDataCell = |
| this.shadowTable_[downCtr + 1][currentShadowNode.j]; |
| } |
| } |
| downShadowNode.colHeaderCells.push(currentCell); |
| } |
| this.tableColHeaders.push(currentCell); |
| } else if (specifiedScope == 'rowgroup') { |
| currentShadowNode.isRowHeader = true; |
| |
| // This cell is a row header for the rest of the cells in this row group. |
| var currentRowGroup = currentShadowNode.rowGroup; |
| |
| // Get the rest of the cells in this row first |
| for (var cellsInRow = currentShadowNode.j + 1; |
| cellsInRow < this.shadowTable_[currentShadowNode.i].length; |
| cellsInRow++) { |
| this.shadowTable_[currentShadowNode.i][cellsInRow]. |
| rowHeaderCells.push(currentCell); |
| } |
| |
| // Now propagate to rest of row group |
| for (var downCtr = currentShadowNode.i + 1; |
| downCtr < this.shadowTable_.length; |
| downCtr++) { |
| |
| if (this.shadowTable_[downCtr][0].rowGroup != currentRowGroup) { |
| break; |
| } |
| |
| for (var rightCtr = 0; |
| rightCtr < this.shadowTable_[downCtr].length; |
| rightCtr++) { |
| |
| this.shadowTable_[downCtr][rightCtr]. |
| rowHeaderCells.push(currentCell); |
| } |
| } |
| this.tableRowHeaders.push(currentCell); |
| |
| } else if (specifiedScope == 'colgroup') { |
| currentShadowNode.isColHeader = true; |
| |
| // This cell is a col header for the rest of the cells in this col group. |
| var currentColGroup = currentShadowNode.colGroup; |
| |
| // Get the rest of the cells in this colgroup first |
| for (var cellsInCol = currentShadowNode.j + 1; |
| cellsInCol < this.shadowTable_[currentShadowNode.i].length; |
| cellsInCol++) { |
| if (this.shadowTable_[currentShadowNode.i][cellsInCol].colGroup == |
| currentColGroup) { |
| this.shadowTable_[currentShadowNode.i][cellsInCol]. |
| colHeaderCells.push(currentCell); |
| } |
| } |
| |
| // Now propagate to rest of col group |
| for (var downCtr = currentShadowNode.i + 1; |
| downCtr < this.shadowTable_.length; |
| downCtr++) { |
| |
| for (var rightCtr = 0; |
| rightCtr < this.shadowTable_[downCtr].length; |
| rightCtr++) { |
| |
| if (this.shadowTable_[downCtr][rightCtr].colGroup == |
| currentColGroup) { |
| this.shadowTable_[downCtr][rightCtr]. |
| colHeaderCells.push(currentCell); |
| } |
| } |
| } |
| this.tableColHeaders.push(currentCell); |
| } |
| if (currentCell.hasAttribute('headers')) { |
| this.findAttrbHeaders_(currentShadowNode); |
| } |
| if (currentCell.hasAttribute('aria-describedby')) { |
| this.findAttrbDescribedBy_(currentShadowNode); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Finds header cells from the 'headers' attribute of a given shadow node's |
| * active cell and classifies them in two ways: |
| * -- Identifies them for the entire table by adding them to |
| * this.tableRowHeaders and this.tableColHeaders. |
| * -- Identifies them for the shadow table node by adding them to the node's |
| * rowHeaderCells or colHeaderCells arrays. |
| * Please note that header cells found through the 'headers' attribute are |
| * difficult to attribute to being either row or column headers because a |
| * table cell can declare arbitrary cells as its headers. A guess is made here |
| * based on which axis the header cell is closest to. |
| * |
| * @param {ShadowTableNode} currentShadowNode A shadow node with an active cell |
| * that has a 'headers' attribute. |
| * |
| * @private |
| */ |
| cvox.TraverseTable.prototype.findAttrbHeaders_ = function(currentShadowNode) { |
| var activeTableCell = currentShadowNode.activeCell; |
| |
| var idList = activeTableCell.getAttribute('headers').split(' '); |
| for (var idToken = 0; idToken < idList.length; idToken++) { |
| // Find cell(s) with this ID, add to header list |
| var idCellArray = cvox.TableUtil.getCellWithID(this.activeTable_, |
| idList[idToken]); |
| |
| for (var idCtr = 0; idCtr < idCellArray.length; idCtr++) { |
| if (idCellArray[idCtr].id == activeTableCell.id) { |
| // Skip if the ID is the same as the current cell's ID |
| break; |
| } |
| // Check if this list of candidate headers contains a |
| // shadowNode with an active cell with this ID already |
| var possibleHeaderNode = |
| this.idToShadowNode_[idCellArray[idCtr].id]; |
| if (! cvox.TableUtil.checkIfHeader(possibleHeaderNode.activeCell)) { |
| // This listed header cell will not be handled later. |
| // Determine whether this is a row or col header for |
| // the active table cell |
| |
| var iDiff = Math.abs(possibleHeaderNode.i - currentShadowNode.i); |
| var jDiff = Math.abs(possibleHeaderNode.j - currentShadowNode.j); |
| if ((iDiff == 0) || (iDiff < jDiff)) { |
| cvox.TableUtil.pushIfNotContained(currentShadowNode.rowHeaderCells, |
| possibleHeaderNode.activeCell); |
| cvox.TableUtil.pushIfNotContained(this.tableRowHeaders, |
| possibleHeaderNode.activeCell); |
| } else { |
| // This is a column header |
| cvox.TableUtil.pushIfNotContained(currentShadowNode.colHeaderCells, |
| possibleHeaderNode.activeCell); |
| cvox.TableUtil.pushIfNotContained(this.tableColHeaders, |
| possibleHeaderNode.activeCell); |
| } |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * Finds header cells from the 'aria-describedby' attribute of a given shadow |
| * node's active cell and classifies them in two ways: |
| * -- Identifies them for the entire table by adding them to |
| * this.tableRowHeaders and this.tableColHeaders. |
| * -- Identifies them for the shadow table node by adding them to the node's |
| * rowHeaderCells or colHeaderCells arrays. |
| * |
| * Please note that header cells found through the 'aria-describedby' attribute |
| * must have the role='rowheader' or role='columnheader' attributes in order to |
| * be considered header cells. |
| * |
| * @param {ShadowTableNode} currentShadowNode A shadow node with an active cell |
| * that has an 'aria-describedby' attribute. |
| * |
| * @private |
| */ |
| cvox.TraverseTable.prototype.findAttrbDescribedBy_ = |
| function(currentShadowNode) { |
| var activeTableCell = currentShadowNode.activeCell; |
| |
| var idList = activeTableCell.getAttribute('aria-describedby').split(' '); |
| for (var idToken = 0; idToken < idList.length; idToken++) { |
| // Find cell(s) with this ID, add to header list |
| var idCellArray = cvox.TableUtil.getCellWithID(this.activeTable_, |
| idList[idToken]); |
| |
| for (var idCtr = 0; idCtr < idCellArray.length; idCtr++) { |
| if (idCellArray[idCtr].id == activeTableCell.id) { |
| // Skip if the ID is the same as the current cell's ID |
| break; |
| } |
| // Check if this list of candidate headers contains a |
| // shadowNode with an active cell with this ID already |
| var possibleHeaderNode = |
| this.idToShadowNode_[idCellArray[idCtr].id]; |
| if (! cvox.TableUtil.checkIfHeader(possibleHeaderNode.activeCell)) { |
| // This listed header cell will not be handled later. |
| // Determine whether this is a row or col header for |
| // the active table cell |
| |
| if (possibleHeaderNode.activeCell.hasAttribute('role') && |
| (possibleHeaderNode.activeCell.getAttribute('role') == |
| 'rowheader')) { |
| cvox.TableUtil.pushIfNotContained(currentShadowNode.rowHeaderCells, |
| possibleHeaderNode.activeCell); |
| cvox.TableUtil.pushIfNotContained(this.tableRowHeaders, |
| possibleHeaderNode.activeCell); |
| } else if (possibleHeaderNode.activeCell.hasAttribute('role') && |
| (possibleHeaderNode.activeCell.getAttribute('role') == |
| 'columnheader')) { |
| cvox.TableUtil.pushIfNotContained(currentShadowNode.colHeaderCells, |
| possibleHeaderNode.activeCell); |
| cvox.TableUtil.pushIfNotContained(this.tableColHeaders, |
| possibleHeaderNode.activeCell); |
| } |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * Gets the current cell or null if there is no current cell. |
| * @return {?Node} The cell <TD> or <TH> or role='gridcell' node. |
| */ |
| cvox.TraverseTable.prototype.getCell = function() { |
| if (!this.currentCellCursor || !this.shadowTable_) { |
| return null; |
| } |
| |
| var shadowEntry = |
| this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; |
| |
| return shadowEntry && shadowEntry.activeCell; |
| }; |
| |
| |
| /** |
| * Gets the cell at the specified location. |
| * @param {Array.<number>} index The index <i, j> of the required cell. |
| * @return {?Node} The cell <TD> or <TH> or role='gridcell' node at the |
| * specified location. Null if that cell does not exist. |
| */ |
| cvox.TraverseTable.prototype.getCellAt = function(index) { |
| if (((index[0] < this.rowCount) && (index[0] >= 0)) && |
| ((index[1] < this.colCount) && (index[1] >= 0))) { |
| var shadowEntry = this.shadowTable_[index[0]][index[1]]; |
| if (shadowEntry != null) { |
| return shadowEntry.activeCell; |
| } |
| } |
| return null; |
| }; |
| |
| |
| /** |
| * Gets the cells that are row headers of the current cell. |
| * @return {!Array} The cells that are row headers of the current cell. Empty if |
| * the current cell does not have row headers. |
| */ |
| cvox.TraverseTable.prototype.getCellRowHeaders = function() { |
| var shadowEntry = |
| this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; |
| |
| return shadowEntry.rowHeaderCells; |
| }; |
| |
| |
| /** |
| * Gets the cells that are col headers of the current cell. |
| * @return {!Array} The cells that are col headers of the current cell. Empty if |
| * the current cell does not have col headers. |
| */ |
| cvox.TraverseTable.prototype.getCellColHeaders = function() { |
| var shadowEntry = |
| this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; |
| |
| return shadowEntry.colHeaderCells; |
| }; |
| |
| |
| /** |
| * Whether or not the current cell is spanned by another cell. |
| * @return {boolean} Whether or not the current cell is spanned by another cell. |
| */ |
| cvox.TraverseTable.prototype.isSpanned = function() { |
| var shadowEntry = |
| this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; |
| |
| return shadowEntry.spanned; |
| }; |
| |
| |
| /** |
| * Whether or not the current cell is a row header cell. |
| * @return {boolean} Whether or not the current cell is a row header cell. |
| */ |
| cvox.TraverseTable.prototype.isRowHeader = function() { |
| var shadowEntry = |
| this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; |
| |
| return shadowEntry.isRowHeader; |
| }; |
| |
| |
| /** |
| * Whether or not the current cell is a col header cell. |
| * @return {boolean} Whether or not the current cell is a col header cell. |
| */ |
| cvox.TraverseTable.prototype.isColHeader = function() { |
| var shadowEntry = |
| this.shadowTable_[this.currentCellCursor[0]][this.currentCellCursor[1]]; |
| |
| return shadowEntry.isColHeader; |
| }; |
| |
| |
| /** |
| * Gets the active column, represented as an array of <TH> or <TD> nodes that |
| * make up a column. In this context, "active" means that this is the column |
| * that contains the cell the user is currently looking at. |
| * @return {Array} An array of <TH> or <TD> or role='gridcell' nodes. |
| */ |
| cvox.TraverseTable.prototype.getCol = function() { |
| var colArray = []; |
| for (var i = 0; i < this.shadowTable_.length; i++) { |
| |
| if (this.shadowTable_[i][this.currentCellCursor[1]]) { |
| var shadowEntry = this.shadowTable_[i][this.currentCellCursor[1]]; |
| |
| if (shadowEntry.colSpan && shadowEntry.rowSpan) { |
| // Look at the last element in the column cell aray. |
| var prev = colArray[colArray.length - 1]; |
| if (prev != |
| shadowEntry.activeCell) { |
| // Watch out for positions spanned by a cell with rowspan and |
| // colspan. We don't want the same cell showing up multiple times |
| // in per-column cell lists. |
| colArray.push( |
| shadowEntry.activeCell); |
| } |
| } else if ((shadowEntry.colSpan) || (!shadowEntry.rowSpan)) { |
| colArray.push( |
| shadowEntry.activeCell); |
| } |
| } |
| } |
| return colArray; |
| }; |
| |
| |
| /** |
| * Gets the active row <TR> node. In this context, "active" means that this is |
| * the row that contains the cell the user is currently looking at. |
| * @return {Node} The active row node. |
| */ |
| cvox.TraverseTable.prototype.getRow = function() { |
| var childRows = cvox.TableUtil.getChildRows(this.activeTable_); |
| return childRows[this.currentCellCursor[0]]; |
| }; |
| |
| |
| /** |
| * Gets the table summary text. |
| * |
| * @return {?string} Either: |
| * 1) The table summary text |
| * 2) Null if the table does not contain a summary attribute. |
| */ |
| cvox.TraverseTable.prototype.summaryText = function() { |
| // see http://code.google.com/p/chromium/issues/detail?id=46567 |
| // for information why this is necessary |
| if (!this.activeTable_.hasAttribute('summary')) { |
| return null; |
| } |
| return this.activeTable_.getAttribute('summary'); |
| }; |
| |
| |
| /** |
| * Gets the table caption text. |
| * |
| * @return {?string} Either: |
| * 1) The table caption text |
| * 2) Null if the table does not include a caption tag. |
| */ |
| cvox.TraverseTable.prototype.captionText = function() { |
| // If there's more than one outer <caption> element, choose the first one. |
| var captionNodes = cvox.XpathUtil.evalXPath('caption\[1]', |
| this.activeTable_); |
| if (captionNodes.length > 0) { |
| return captionNodes[0].innerHTML; |
| } else { |
| return null; |
| } |
| }; |
| |
| |
| /** |
| * Calculates the number of columns in the shadow table. |
| * @return {number} The number of columns in the shadow table. |
| * @private |
| */ |
| cvox.TraverseTable.prototype.shadowColCount_ = function() { |
| // As the shadow table is a 2D array, the number of columns is the |
| // max number of elements in the second-level arrays. |
| var max = 0; |
| for (var i = 0; i < this.shadowTable_.length; i++) { |
| if (this.shadowTable_[i].length > max) { |
| max = this.shadowTable_[i].length; |
| } |
| } |
| return max; |
| }; |
| |
| |
| /** |
| * Calculates the number of rows in the table. |
| * @return {number} The number of rows in the table. |
| * @private |
| */ |
| cvox.TraverseTable.prototype.countRows_ = function() { |
| // Number of rows in a table is equal to the number of TR elements contained |
| // by the (outer) TBODY elements. |
| var rowCount = cvox.TableUtil.getChildRows(this.activeTable_); |
| return rowCount.length; |
| }; |
| |
| |
| /** |
| * Calculates the number of columns in the table. |
| * This uses the W3C recommended algorithm for calculating number of |
| * columns, but it does not take rowspans or colspans into account. This means |
| * that the number of columns calculated here might be lower than the actual |
| * number of columns in the table if columns are indicated by colspans. |
| * @return {number} The number of columns in the table. |
| * @private |
| */ |
| cvox.TraverseTable.prototype.getW3CColCount_ = function() { |
| // See http://www.w3.org/TR/html401/struct/tables.html#h-11.2.4.3 |
| |
| var colgroupNodes = cvox.XpathUtil.evalXPath('child::colgroup', |
| this.activeTable_); |
| var colNodes = cvox.XpathUtil.evalXPath('child::col', this.activeTable_); |
| |
| if ((colgroupNodes.length == 0) && (colNodes.length == 0)) { |
| var maxcols = 0; |
| var outerChildren = cvox.TableUtil.getChildRows(this.activeTable_); |
| for (var i = 0; i < outerChildren.length; i++) { |
| var childrenCount = cvox.TableUtil.getChildCells(outerChildren[i]); |
| if (childrenCount.length > maxcols) { |
| maxcols = childrenCount.length; |
| } |
| } |
| return maxcols; |
| } else { |
| var sum = 0; |
| for (var i = 0; i < colNodes.length; i++) { |
| if (colNodes[i].hasAttribute('span')) { |
| sum += colNodes[i].getAttribute('span'); |
| } else { |
| sum += 1; |
| } |
| } |
| for (i = 0; i < colgroupNodes.length; i++) { |
| var colChildren = cvox.XpathUtil.evalXPath('child::col', |
| colgroupNodes[i]); |
| if (colChildren.length == 0) { |
| if (colgroupNodes[i].hasAttribute('span')) { |
| sum += colgroupNodes[i].getAttribute('span'); |
| } else { |
| sum += 1; |
| } |
| } |
| } |
| } |
| return sum; |
| }; |
| |
| |
| /** |
| * Moves to the next row in the table. Updates the cell cursor. |
| * |
| * @return {boolean} Either: |
| * 1) True if the update has been made. |
| * 2) False if the end of the table has been reached and the update has not |
| * happened. |
| */ |
| cvox.TraverseTable.prototype.nextRow = function() { |
| if (!this.currentCellCursor) { |
| // We have not started moving through the table yet |
| return this.goToRow(0); |
| } else { |
| return this.goToRow(this.currentCellCursor[0] + 1); |
| } |
| |
| }; |
| |
| |
| /** |
| * Moves to the previous row in the table. Updates the cell cursor. |
| * |
| * @return {boolean} Either: |
| * 1) True if the update has been made. |
| * 2) False if the end of the table has been reached and the update has not |
| * happened. |
| */ |
| cvox.TraverseTable.prototype.prevRow = function() { |
| if (!this.currentCellCursor) { |
| // We have not started moving through the table yet |
| return this.goToRow(this.rowCount - 1); |
| } else { |
| return this.goToRow(this.currentCellCursor[0] - 1); |
| } |
| }; |
| |
| |
| /** |
| * Moves to the next column in the table. Updates the cell cursor. |
| * |
| * @return {boolean} Either: |
| * 1) True if the update has been made. |
| * 2) False if the end of the table has been reached and the update has not |
| * happened. |
| */ |
| cvox.TraverseTable.prototype.nextCol = function() { |
| if (!this.currentCellCursor) { |
| // We have not started moving through the table yet |
| return this.goToCol(0); |
| } else { |
| return this.goToCol(this.currentCellCursor[1] + 1); |
| } |
| }; |
| |
| |
| /** |
| * Moves to the previous column in the table. Updates the cell cursor. |
| * |
| * @return {boolean} Either: |
| * 1) True if the update has been made. |
| * 2) False if the end of the table has been reached and the update has not |
| * happened. |
| */ |
| cvox.TraverseTable.prototype.prevCol = function() { |
| if (!this.currentCellCursor) { |
| // We have not started moving through the table yet |
| return this.goToCol(this.shadowColCount_() - 1); |
| } else { |
| return this.goToCol(this.currentCellCursor[1] - 1); |
| } |
| }; |
| |
| |
| /** |
| * Moves to the row at the specified index in the table. Updates the cell |
| * cursor. |
| * @param {number} index The index of the required row. |
| * @return {boolean} Either: |
| * 1) True if the index is valid and the update has been made. |
| * 2) False if the index is not valid (either less than 0 or greater than |
| * the number of rows in the table). |
| */ |
| cvox.TraverseTable.prototype.goToRow = function(index) { |
| if (this.shadowTable_[index] != null) { |
| if (this.currentCellCursor == null) { |
| // We haven't started moving through the table yet |
| this.currentCellCursor = [index, 0]; |
| } else { |
| this.currentCellCursor = [index, this.currentCellCursor[1]]; |
| } |
| return true; |
| } else { |
| return false; |
| } |
| }; |
| |
| |
| /** |
| * Moves to the column at the specified index in the table. Updates the cell |
| * cursor. |
| * @param {number} index The index of the required column. |
| * @return {boolean} Either: |
| * 1) True if the index is valid and the update has been made. |
| * 2) False if the index is not valid (either less than 0 or greater than |
| * the number of rows in the table). |
| */ |
| cvox.TraverseTable.prototype.goToCol = function(index) { |
| if (index < 0 || index >= this.colCount) { |
| return false; |
| } |
| if (this.currentCellCursor == null) { |
| // We haven't started moving through the table yet |
| this.currentCellCursor = [0, index]; |
| } else { |
| this.currentCellCursor = [this.currentCellCursor[0], index]; |
| } |
| return true; |
| }; |
| |
| |
| /** |
| * Moves to the cell at the specified index <i, j> in the table. Updates the |
| * cell cursor. |
| * @param {Array.<number>} index The index <i, j> of the required cell. |
| * @return {boolean} Either: |
| * 1) True if the index is valid and the update has been made. |
| * 2) False if the index is not valid (either less than 0, greater than |
| * the number of rows or columns in the table, or there is no cell |
| * at that location). |
| */ |
| cvox.TraverseTable.prototype.goToCell = function(index) { |
| if (((index[0] < this.rowCount) && (index[0] >= 0)) && |
| ((index[1] < this.colCount) && (index[1] >= 0))) { |
| var cell = this.shadowTable_[index[0]][index[1]]; |
| if (cell != null) { |
| this.currentCellCursor = index; |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Moves to the cell at the last index in the table. Updates the cell cursor. |
| * @return {boolean} Either: |
| * 1) True if the index is valid and the update has been made. |
| * 2) False if the index is not valid (there is no cell at that location). |
| */ |
| cvox.TraverseTable.prototype.goToLastCell = function() { |
| var numRows = this.shadowTable_.length; |
| if (numRows == 0) { |
| return false; |
| } |
| var lastRow = this.shadowTable_[numRows - 1]; |
| var lastIndex = [(numRows - 1), (lastRow.length - 1)]; |
| var cell = |
| this.shadowTable_[lastIndex[0]][lastIndex[1]]; |
| if (cell != null) { |
| this.currentCellCursor = lastIndex; |
| return true; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Moves to the cell at the last index in the current row of the table. Update |
| * the cell cursor. |
| * @return {boolean} Either: |
| * 1) True if the index is valid and the update has been made. |
| * 2) False if the index is not valid (there is no cell at that location). |
| */ |
| cvox.TraverseTable.prototype.goToRowLastCell = function() { |
| var currentRow = this.currentCellCursor[0]; |
| var lastIndex = [currentRow, (this.shadowTable_[currentRow].length - 1)]; |
| var cell = |
| this.shadowTable_[lastIndex[0]][lastIndex[1]]; |
| if (cell != null) { |
| this.currentCellCursor = lastIndex; |
| return true; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Moves to the cell at the last index in the current column of the table. |
| * Update the cell cursor. |
| * @return {boolean} Either: |
| * 1) True if the index is valid and the update has been made. |
| * 2) False if the index is not valid (there is no cell at that location). |
| */ |
| cvox.TraverseTable.prototype.goToColLastCell = function() { |
| var currentCol = this.getCol(); |
| var lastIndex = [(currentCol.length - 1), this.currentCellCursor[1]]; |
| var cell = |
| this.shadowTable_[lastIndex[0]][lastIndex[1]]; |
| if (cell != null) { |
| this.currentCellCursor = lastIndex; |
| return true; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Resets the table cursors. |
| * |
| */ |
| cvox.TraverseTable.prototype.resetCursor = function() { |
| this.currentCellCursor = null; |
| }; |